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 = runCatching { + override suspend fun pinUserIdentity(userId: UserId): Result = runCatchingExceptions { getUserIdentityInternal(userId).pin() } - override suspend fun withdrawVerification(userId: UserId): Result = runCatching { + override suspend fun withdrawVerification(userId: UserId): Result = runCatchingExceptions { getUserIdentityInternal(userId).withdrawVerification() } - override suspend fun getUserIdentity(userId: UserId): Result = runCatching { + override suspend fun getUserIdentity(userId: UserId): Result = runCatchingExceptions { val identity = getUserIdentityInternal(userId) val isVerified = identity.isVerified() when { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt index e16c094569..3651cc52ac 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/encryption/RustIdentityResetHandle.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.impl.encryption +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.encryption.IdentityOidcResetHandle import io.element.android.libraries.matrix.api.encryption.IdentityPasswordResetHandle @@ -20,7 +21,7 @@ object RustIdentityResetHandleFactory { userId: UserId, identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle? ): Result { - return runCatching { + return runCatchingExceptions { identityResetHandle?.let { when (val authType = identityResetHandle.authType()) { is CrossSigningResetAuthType.Oidc -> RustOidcIdentityResetHandle(identityResetHandle, authType.info.approvalUrl) @@ -37,7 +38,7 @@ class RustPasswordIdentityResetHandle( private val identityResetHandle: org.matrix.rustcomponents.sdk.IdentityResetHandle, ) : IdentityPasswordResetHandle { override suspend fun resetPassword(password: String): Result { - return runCatching { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) } + return runCatchingExceptions { identityResetHandle.reset(AuthData.Password(AuthDataPasswordDetails(userId.value, password))) } } override suspend fun cancel() { @@ -50,7 +51,7 @@ class RustOidcIdentityResetHandle( override val url: String, ) : IdentityOidcResetHandle { override suspend fun resetOidc(): Result { - return runCatching { identityResetHandle.reset(null) } + return runCatchingExceptions { identityResetHandle.reset(null) } } override suspend fun cancel() { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaUploadHandlerImpl.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaUploadHandlerImpl.kt index 185bdf42c7..dbfa67f374 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaUploadHandlerImpl.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaUploadHandlerImpl.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.media import io.element.android.libraries.androidutils.file.safeDelete +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.media.MediaUploadHandler import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import java.io.File @@ -17,7 +18,7 @@ class MediaUploadHandlerImpl( private val sendAttachmentJoinHandle: SendAttachmentJoinHandle, ) : MediaUploadHandler { override suspend fun await(): Result = - runCatching { + runCatchingExceptions { sendAttachmentJoinHandle.join() } .also { cleanUpFiles() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt index a7a926239b..d846321811 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.media 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.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile @@ -34,7 +35,7 @@ class RustMediaLoader( @OptIn(ExperimentalUnsignedTypes::class) override suspend fun loadMediaContent(source: MediaSource): Result = withContext(mediaDispatcher) { - runCatching { + runCatchingExceptions { source.toRustMediaSource().use { source -> innerClient.getMediaContent(source) } @@ -48,7 +49,7 @@ class RustMediaLoader( height: Long ): Result = withContext(mediaDispatcher) { - runCatching { + runCatchingExceptions { source.toRustMediaSource().use { mediaSource -> innerClient.getMediaThumbnail( mediaSource = mediaSource, @@ -66,12 +67,20 @@ class RustMediaLoader( useCache: Boolean, ): Result = withContext(mediaDispatcher) { - runCatching { + runCatchingExceptions { source.toRustMediaSource().use { mediaSource -> val mediaFile = innerClient.getMediaFile( mediaSource = mediaSource, filename = filename, - mimeType = mimeType?.takeIf { MimeTypes.hasSubtype(it) } ?: MimeTypes.OctetStream, + mimeType = when { + mimeType == null -> MimeTypes.OctetStream + MimeTypes.hasSubtype(mimeType) -> mimeType + // Fallback to a default mime type based on the main type, so that the SDK can create a file with the correct extension. + mimeType == MimeTypes.Images -> MimeTypes.Jpeg + mimeType == MimeTypes.Videos -> MimeTypes.Mp4 + mimeType == MimeTypes.Audio -> MimeTypes.Mp3 + else -> MimeTypes.OctetStream + }, useCache = useCache, tempDir = cacheDirectory.path, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt index 7cf432bad4..bab7cdea02 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.notification import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId @@ -30,7 +31,7 @@ class RustNotificationService( override suspend fun getNotifications( ids: Map> ): Result> = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { val requests = ids.map { (roomId, eventIds) -> NotificationItemsRequest( roomId = roomId.value, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt index 5cfd9741dd..b4f3e70754 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsService.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl.notificationsettings import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.suspendLazy +import io.element.android.libraries.core.extensions.runCatchingExceptions 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.room.RoomNotificationMode @@ -48,12 +49,12 @@ class RustNotificationSettingsService( } override suspend fun getRoomNotificationSettings(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean): Result = - runCatching { + runCatchingExceptions { notificationSettings.await().getRoomNotificationSettings(roomId.value, isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::map) } override suspend fun getDefaultRoomNotificationMode(isEncrypted: Boolean, isOneToOne: Boolean): Result = - runCatching { + runCatchingExceptions { notificationSettings.await().getDefaultRoomNotificationMode(isEncrypted, isOneToOne).let(RoomNotificationSettingsMapper::mapMode) } @@ -62,7 +63,7 @@ class RustNotificationSettingsService( mode: RoomNotificationMode, isOneToOne: Boolean ): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { try { notificationSettings.await().setDefaultRoomNotificationMode(isEncrypted, isOneToOne, mode.let(RoomNotificationSettingsMapper::mapMode)) } catch (exception: NotificationSettingsException.RuleNotFound) { @@ -74,13 +75,13 @@ class RustNotificationSettingsService( } override suspend fun setRoomNotificationMode(roomId: RoomId, mode: RoomNotificationMode): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { notificationSettings.await().setRoomNotificationMode(roomId.value, mode.let(RoomNotificationSettingsMapper::mapMode)) } } override suspend fun restoreDefaultRoomNotificationMode(roomId: RoomId): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { notificationSettings.await().restoreDefaultRoomNotificationMode(roomId.value) } } @@ -88,54 +89,54 @@ class RustNotificationSettingsService( override suspend fun muteRoom(roomId: RoomId): Result = setRoomNotificationMode(roomId, RoomNotificationMode.MUTE) override suspend fun unmuteRoom(roomId: RoomId, isEncrypted: Boolean, isOneToOne: Boolean) = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { notificationSettings.await().unmuteRoom(roomId.value, isEncrypted, isOneToOne) } } override suspend fun isRoomMentionEnabled(): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { notificationSettings.await().isRoomMentionEnabled() } } override suspend fun setRoomMentionEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { notificationSettings.await().setRoomMentionEnabled(enabled) } } override suspend fun isCallEnabled(): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { notificationSettings.await().isCallEnabled() } } override suspend fun setCallEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { notificationSettings.await().setCallEnabled(enabled) } } override suspend fun isInviteForMeEnabled(): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { notificationSettings.await().isInviteForMeEnabled() } } override suspend fun setInviteForMeEnabled(enabled: Boolean): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { notificationSettings.await().setInviteForMeEnabled(enabled) } } override suspend fun getRoomsWithUserDefinedRules(): Result> = - runCatching { + runCatchingExceptions { notificationSettings.await().getRoomsWithUserDefinedRules(enabled = true) } override suspend fun canHomeServerPushEncryptedEventsToDevice(): Result = - runCatching { + runCatchingExceptions { notificationSettings.await().canPushEncryptedEventToDevice() } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilder.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilder.kt index 50b3eb1d3e..eb0524886f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilder.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkBuilder.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.permalink 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.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.RoomAlias @@ -24,7 +25,7 @@ class DefaultPermalinkBuilder @Inject constructor() : PermalinkBuilder { if (!MatrixPatterns.isUserId(userId.value)) { return Result.failure(PermalinkBuilderError.InvalidData) } - return runCatching { + return runCatchingExceptions { matrixToUserPermalink(userId.value) } } @@ -33,7 +34,7 @@ class DefaultPermalinkBuilder @Inject constructor() : PermalinkBuilder { if (!MatrixPatterns.isRoomAlias(roomAlias.value)) { return Result.failure(PermalinkBuilderError.InvalidData) } - return runCatching { + return runCatchingExceptions { matrixToRoomAliasPermalink(roomAlias.value) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt index 80f59b9469..0bf3e908b6 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/permalink/DefaultPermalinkParser.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl.permalink import androidx.core.net.toUri 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.core.EventId import io.element.android.libraries.matrix.api.core.RoomAlias @@ -44,7 +45,7 @@ class DefaultPermalinkParser @Inject constructor( // so convert URI to matrix.to to simplify parsing process val matrixToUri = matrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri) - val result = runCatching { + val result = runCatchingExceptions { parseMatrixEntityFrom(matrixToUri.toString()) }.getOrNull() return if (result == null) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt index 3ff24e85d8..0fed390b14 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl.pushers import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.extensions.mapFailure +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData @@ -26,7 +27,7 @@ class RustPushersService( ) : PushersService { override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result { return withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { client.setPusher( identifiers = PusherIdentifiers( pushkey = setHttpPusherData.pushKey, @@ -51,7 +52,7 @@ class RustPushersService( override suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result { return withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { client.deletePusher( identifiers = PusherIdentifiers( pushkey = unsetHttpPusherData.pushKey, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt index cca8af2785..18e6571a27 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/JoinedRustRoom.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.impl.room import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.childScope import io.element.android.libraries.core.extensions.mapFailure +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.EventId @@ -204,7 +205,7 @@ class JoinedRustRoom( // Track read receipts only for focused timeline for performance optimization val trackReadReceipts = createTimelineParams is CreateTimelineParams.Focused - runCatching { + runCatchingExceptions { innerRoom.timelineWithConfiguration( configuration = TimelineConfiguration( focus = focus, @@ -243,7 +244,7 @@ class JoinedRustRoom( htmlBody: String?, intentionalMentions: List ): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { MessageEventContent.from(body, htmlBody, intentionalMentions).use { newContent -> innerRoom.edit(eventId.value, newContent) } @@ -251,43 +252,43 @@ class JoinedRustRoom( } override suspend fun typingNotice(isTyping: Boolean) = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.typingNotice(isTyping) } } override suspend fun inviteUserById(id: UserId): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.inviteUserById(id.value) } } override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.uploadAvatar(mimeType, data, null) } } override suspend fun removeAvatar(): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.removeAvatar() } } override suspend fun setName(name: String): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.setName(name) } } override suspend fun setTopic(topic: String): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.setTopic(topic) } } override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason) if (blockUserId != null) { innerRoom.ignoreUser(blockUserId.value) @@ -299,7 +300,7 @@ class JoinedRustRoom( val currentState = roomNotificationSettingsStateFlow.value val currentRoomNotificationSettings = currentState.roomNotificationSettings() roomNotificationSettingsStateFlow.value = RoomNotificationSettingsState.Pending(prevRoomNotificationSettings = currentRoomNotificationSettings) - runCatching { + runCatchingExceptions { val isEncrypted = roomInfoFlow.value.isEncrypted ?: getUpdatedIsEncrypted().getOrThrow() notificationSettingsService.getRoomNotificationSettings(roomId, isEncrypted, isOneToOne).getOrThrow() }.map { @@ -313,56 +314,56 @@ class JoinedRustRoom( } override suspend fun updateCanonicalAlias(canonicalAlias: RoomAlias?, alternativeAliases: List): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.updateCanonicalAlias(canonicalAlias?.value, alternativeAliases.map { it.value }) } } override suspend fun publishRoomAliasInRoomDirectory(roomAlias: RoomAlias): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.publishRoomAliasInRoomDirectory(roomAlias.value) } } override suspend fun removeRoomAliasFromRoomDirectory(roomAlias: RoomAlias): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.removeRoomAliasFromRoomDirectory(roomAlias.value) } } override suspend fun updateRoomVisibility(roomVisibility: RoomVisibility): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.updateRoomVisibility(roomVisibility.map()) } } override suspend fun updateHistoryVisibility(historyVisibility: RoomHistoryVisibility): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.updateHistoryVisibility(historyVisibility.map()) } } override suspend fun enableEncryption(): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.enableEncryption() } } override suspend fun updateJoinRule(joinRule: JoinRule): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.updateJoinRules(joinRule.map()) } } override suspend fun updateUsersRoles(changes: List): Result { - return runCatching { + return runCatchingExceptions { val powerLevelChanges = changes.map { UserPowerLevelUpdate(it.userId.value, it.powerLevel) } innerRoom.updatePowerLevelsForUsers(powerLevelChanges) } } override suspend fun updatePowerLevels(roomPowerLevels: RoomPowerLevels): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { val changes = RoomPowerLevelChanges( ban = roomPowerLevels.ban, invite = roomPowerLevels.invite, @@ -378,25 +379,25 @@ class JoinedRustRoom( } override suspend fun resetPowerLevels(): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { RoomPowerLevelsMapper.map(innerRoom.resetPowerLevels()) } } override suspend fun kickUser(userId: UserId, reason: String?): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.kickUser(userId.value, reason) } } override suspend fun banUser(userId: UserId, reason: String?): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.banUser(userId.value, reason) } } override suspend fun unbanUser(userId: UserId, reason: String?): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.unbanUser(userId.value, reason) } } @@ -407,13 +408,13 @@ class JoinedRustRoom( languageTag: String?, theme: String?, ) = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { widgetSettings.generateWidgetWebViewUrl(innerRoom, clientId, languageTag, theme) } } override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result { - return runCatching { + return runCatchingExceptions { RustWidgetDriver( widgetSettings = widgetSettings, room = innerRoom, @@ -427,7 +428,7 @@ class JoinedRustRoom( } override suspend fun sendCallNotificationIfNeeded(): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.sendCallNotificationIfNeeded() } } @@ -435,14 +436,14 @@ class JoinedRustRoom( override suspend fun setSendQueueEnabled(enabled: Boolean) { withContext(roomDispatcher) { Timber.d("setSendQueuesEnabled: $enabled") - runCatching { + runCatchingExceptions { innerRoom.enableSendQueue(enabled) } } } override suspend fun ignoreDeviceTrustAndResend(devices: Map>, sendHandle: SendHandle) = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.ignoreDeviceTrustAndResend( devices = devices.entries.associate { entry -> entry.key.value to entry.value.map { it.value } @@ -453,7 +454,7 @@ class JoinedRustRoom( } override suspend fun withdrawVerificationAndResend(userIds: List, sendHandle: SendHandle) = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.withdrawVerificationAndResend( userIds = userIds.map { it.value }, sendHandle = (sendHandle as RustSendHandle).inner, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/NotJoinedRustRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/NotJoinedRustRoom.kt index 3f3cb4f6cd..fb88cf625b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/NotJoinedRustRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/NotJoinedRustRoom.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.room import androidx.compose.runtime.Immutable +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.room.NotJoinedRoom import io.element.android.libraries.matrix.api.room.RoomMembershipDetails @@ -20,8 +21,8 @@ class NotJoinedRustRoom( override val localRoom: RustBaseRoom?, override val previewInfo: RoomPreviewInfo, ) : NotJoinedRoom { - override suspend fun membershipDetails(): Result = runCatching { - val room = localRoom?.innerRoom ?: return@runCatching null + override suspend fun membershipDetails(): Result = runCatchingExceptions { + val room = localRoom?.innerRoom ?: return@runCatchingExceptions null val (ownMember, senderInfo) = room.memberWithSenderInfo(sessionId.value) RoomMembershipDetails( currentUserMember = RoomMemberMapper.map(ownMember), diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt index 63f22dbf7d..9d7495691d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.room import io.element.android.libraries.core.coroutine.parallelMap +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.ForwardEventException @@ -52,7 +53,7 @@ class RoomContentForwarder( val failedForwardingTo = mutableSetOf() targetRooms.parallelMap { room -> room.use { targetRoom -> - runCatching { + runCatchingExceptions { // Sending a message requires a registered timeline listener targetRoom.timeline().runWithTimelineListenerRegistered { withTimeout(timeoutMs.milliseconds) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt index 3a7a78d444..f86a179af2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoom.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl.room import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.childScope +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.DeviceId import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -90,7 +91,7 @@ class RustBaseRoom( } override suspend fun getMembers(limit: Int) = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.members().use { it.nextChunk(limit.toUInt()).orEmpty().map { roomMember -> RoomMemberMapper.map(roomMember) @@ -100,7 +101,7 @@ class RustBaseRoom( } override suspend fun getUpdatedMember(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { RoomMemberMapper.map(innerRoom.member(userId.value)) } } @@ -113,32 +114,32 @@ class RustBaseRoom( } override suspend fun userDisplayName(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.memberDisplayName(userId.value) } } override suspend fun userRole(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { RoomMemberMapper.mapRole(innerRoom.suggestedRoleForUser(userId.value)) } } override suspend fun powerLevels(): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { RoomPowerLevelsMapper.map(innerRoom.getPowerLevels()) } } override suspend fun userAvatarUrl(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.memberAvatarUrl(userId.value) } } override suspend fun leave(): Result = withContext(roomDispatcher) { val membershipBeforeLeft = roomInfoFlow.value.currentUserMembership - runCatching { + runCatchingExceptions { innerRoom.leave() }.onSuccess { roomMembershipObserver.notifyUserLeftRoom(roomId, membershipBeforeLeft) @@ -146,142 +147,142 @@ class RustBaseRoom( } override suspend fun join(): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.join() } } override suspend fun forget(): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.forget() } } override suspend fun canUserInvite(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.canUserInvite(userId.value) } } override suspend fun canUserKick(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.canUserKick(userId.value) } } override suspend fun canUserBan(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.canUserBan(userId.value) } } override suspend fun canUserRedactOwn(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.canUserRedactOwn(userId.value) } } override suspend fun canUserRedactOther(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.canUserRedactOther(userId.value) } } override suspend fun canUserSendState(userId: UserId, type: StateEventType): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.canUserSendState(userId.value, type.map()) } } override suspend fun canUserSendMessage(userId: UserId, type: MessageEventType): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.canUserSendMessage(userId.value, type.map()) } } override suspend fun canUserTriggerRoomNotification(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.canUserTriggerRoomNotification(userId.value) } } override suspend fun canUserPinUnpin(userId: UserId): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.canUserPinUnpin(userId.value) } } override suspend fun clearEventCacheStorage(): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.clearEventCacheStorage() } } override suspend fun setIsFavorite(isFavorite: Boolean): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.setIsFavourite(isFavorite, null) } } override suspend fun markAsRead(receiptType: ReceiptType): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.markAsRead(receiptType.toRustReceiptType()) } } override suspend fun setUnreadFlag(isUnread: Boolean): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.setUnreadFlag(isUnread) } } override suspend fun getPermalink(): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.matrixToPermalink() } } override suspend fun getPermalinkFor(eventId: EventId): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.matrixToEventPermalink(eventId.value) } } override suspend fun getRoomVisibility(): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.getRoomVisibility().map() } } override suspend fun getUpdatedIsEncrypted(): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { innerRoom.latestEncryptionState() == EncryptionState.ENCRYPTED } } override suspend fun saveComposerDraft(composerDraft: ComposerDraft): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { Timber.d("saveComposerDraft: $composerDraft into $roomId") innerRoom.saveComposerDraft(composerDraft.into()) } } override suspend fun loadComposerDraft(): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { Timber.d("loadComposerDraft for $roomId") innerRoom.loadComposerDraft()?.into() } } override suspend fun clearComposerDraft(): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { Timber.d("clearComposerDraft for $roomId") innerRoom.clearComposerDraft() } } override suspend fun reportRoom(reason: String?): Result = withContext(roomDispatcher) { - runCatching { + runCatchingExceptions { Timber.d("reportRoom $roomId") innerRoom.reportRoom(reason) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt index e5a8ff4eb0..4ea1611b38 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomFactory.kt @@ -51,13 +51,12 @@ class RustRoomFactory( private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory, private val featureFlagService: FeatureFlagService, private val roomMembershipObserver: RoomMembershipObserver, + private val roomInfoMapper: RoomInfoMapper, ) { private val dispatcher = dispatchers.io.limitedParallelism(1) private val mutex = Mutex() private val isDestroyed: AtomicBoolean = AtomicBoolean(false) - private val roomInfoMapper = RoomInfoMapper() - private val eventFilters = TimelineConfig.excludedEvents .takeIf { it.isNotEmpty() } ?.let { listStateEventType -> diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoom.kt index a93ffad91e..0abe45c0fa 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoom.kt @@ -13,7 +13,6 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.room.join.JoinRoom -import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.impl.analytics.toAnalyticsJoinedRoom import io.element.android.services.analytics.api.AnalyticsService import javax.inject.Inject @@ -39,15 +38,10 @@ class DefaultJoinRoom @Inject constructor( is RoomIdOrAlias.Alias -> { client.joinRoomByIdOrAlias(roomIdOrAlias, serverNames = emptyList()) } - }.onSuccess { roomSummary -> - client.captureJoinedRoomAnalytics(roomSummary, trigger) + }.onSuccess { roomInfo -> + if (roomInfo != null) { + analyticsService.capture(roomInfo.toAnalyticsJoinedRoom(trigger)) + } }.map { } } - - private suspend fun MatrixClient.captureJoinedRoomAnalytics(roomSummary: RoomSummary?, trigger: JoinedRoom.Trigger) { - if (roomSummary == null) return - getRoom(roomSummary.roomId)?.use { room -> - analyticsService.capture(room.toAnalyticsJoinedRoom(trigger)) - } - } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt index 70167ec45c..02e405969b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt @@ -7,6 +7,7 @@ package io.element.android.libraries.matrix.impl.room.knock +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.knock.KnockRequest @@ -23,19 +24,19 @@ class RustKnockRequest( override val timestamp: Long? = inner.timestamp?.toLong() override val isSeen: Boolean = inner.isSeen - override suspend fun accept(): Result = runCatching { + override suspend fun accept(): Result = runCatchingExceptions { inner.actions.accept() } - override suspend fun decline(reason: String?): Result = runCatching { + override suspend fun decline(reason: String?): Result = runCatchingExceptions { inner.actions.decline(reason) } - override suspend fun declineAndBan(reason: String?): Result = runCatching { + override suspend fun declineAndBan(reason: String?): Result = runCatchingExceptions { inner.actions.declineAndBan(reason) } - override suspend fun markAsSeen(): Result = runCatching { + override suspend fun markAsSeen(): Result = runCatchingExceptions { inner.actions.markAsSeen() } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt index adcdfee9e1..8cdcd13f22 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListExtensions.kt @@ -116,7 +116,7 @@ internal fun RoomListServiceInterface.syncIndicator(): Flow = timelineDiffProcessor.membershipChangeEventReceived override suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.sendReadReceipt(receiptType.toRustReceiptType(), eventId.value) } } @@ -181,7 +182,7 @@ class RustTimeline( override suspend fun paginate(direction: Timeline.PaginationDirection): Result = withContext(NonCancellable) { withContext(dispatcher) { initLatch.await() - runCatching { + runCatchingExceptions { if (!canPaginate(direction)) throw TimelineException.CannotPaginate updatePaginationStatus(direction) { it.copy(isPaginating = true) } when (direction) { @@ -275,14 +276,14 @@ class RustTimeline( intentionalMentions: List, ): Result = withContext(dispatcher) { MessageEventContent.from(body, htmlBody, intentionalMentions).use { content -> - runCatching { + runCatchingExceptions { inner.send(content) } } } override suspend fun redactEvent(eventOrTransactionId: EventOrTransactionId, reason: String?): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.redactEvent( eventOrTransactionId = eventOrTransactionId.toRustEventOrTransactionId(), reason = reason, @@ -296,7 +297,7 @@ class RustTimeline( htmlBody: String?, intentionalMentions: List, ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { val editedContent = EditedContent.RoomMessage( content = MessageEventContent.from( body = body, @@ -316,7 +317,7 @@ class RustTimeline( caption: String?, formattedCaption: String?, ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { val editedContent = EditedContent.MediaCaption( caption = caption, formattedCaption = formattedCaption?.let { @@ -340,7 +341,7 @@ class RustTimeline( intentionalMentions: List, fromNotification: Boolean, ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { val msg = MessageEventContent.from(body, htmlBody, intentionalMentions) inner.sendReply( msg = msg, @@ -462,7 +463,7 @@ class RustTimeline( } override suspend fun toggleReaction(emoji: String, eventOrTransactionId: EventOrTransactionId): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.toggleReaction( key = emoji, itemId = eventOrTransactionId.toRustEventOrTransactionId(), @@ -471,7 +472,7 @@ class RustTimeline( } override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { roomContentForwarder.forward(fromTimeline = inner, eventId = eventId, toRoomIds = roomIds) }.onFailure { Timber.e(it) @@ -485,7 +486,7 @@ class RustTimeline( zoomLevel: Int?, assetType: AssetType?, ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.sendLocation( body = body, geoUri = geoUri, @@ -528,7 +529,7 @@ class RustTimeline( maxSelections: Int, pollKind: PollKind, ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.createPoll( question = question, answers = answers, @@ -545,7 +546,7 @@ class RustTimeline( maxSelections: Int, pollKind: PollKind, ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { val editedContent = EditedContent.PollStart( pollData = PollData( question = question, @@ -565,7 +566,7 @@ class RustTimeline( pollStartId: EventId, answers: List ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.sendPollResponse( pollStartEventId = pollStartId.value, answers = answers, @@ -577,7 +578,7 @@ class RustTimeline( pollStartId: EventId, text: String ): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.endPoll( pollStartEventId = pollStartId.value, text = text, @@ -586,7 +587,7 @@ class RustTimeline( } private fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { - return runCatching { + return runCatchingExceptions { MediaUploadHandlerImpl(files, handle()) } } @@ -609,19 +610,19 @@ class RustTimeline( } override suspend fun pinEvent(eventId: EventId): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.pinEvent(eventId = eventId.value) } } override suspend fun unpinEvent(eventId: EventId): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.unpinEvent(eventId = eventId.value) } } private suspend fun fetchDetailsForEvent(eventId: EventId): Result = withContext(dispatcher) { - runCatching { + runCatchingExceptions { inner.fetchDetailsForEvent(eventId.value) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index 7cd12d0f38..d806c9492f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -31,6 +31,9 @@ import org.matrix.rustcomponents.sdk.FormattedBody as RustFormattedBody import org.matrix.rustcomponents.sdk.MessageFormat as RustMessageFormat import org.matrix.rustcomponents.sdk.MessageType as RustMessageType +// https://github.com/Johennes/matrix-spec-proposals/blob/johannes/msgtype-galleries/proposals/4274-inline-media-galleries.md#unstable-prefix +private const val MSG_TYPE_GALLERY_UNSTABLE = "dm.filament.gallery" + class EventMessageMapper { private val inReplyToMapper by lazy { InReplyToMapper(TimelineEventContentMapper()) } @@ -112,6 +115,10 @@ class EventMessageMapper { is MessageType.Other -> { OtherMessageType(type.msgtype, type.body) } + is MessageType.Gallery -> { + // TODO expose the GalleryType. + OtherMessageType(MSG_TYPE_GALLERY_UNSTABLE, type.content.body) + } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/reply/InReplyToMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/reply/InReplyToMapper.kt index 052385e5be..1b329bfe0a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/reply/InReplyToMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/reply/InReplyToMapper.kt @@ -12,8 +12,8 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper import io.element.android.libraries.matrix.impl.timeline.item.event.map +import org.matrix.rustcomponents.sdk.EmbeddedEventDetails import org.matrix.rustcomponents.sdk.InReplyToDetails -import org.matrix.rustcomponents.sdk.RepliedToEventDetails class InReplyToMapper( private val timelineEventContentMapper: TimelineEventContentMapper, @@ -21,7 +21,7 @@ class InReplyToMapper( fun map(inReplyToDetails: InReplyToDetails): InReplyTo { val inReplyToId = EventId(inReplyToDetails.eventId()) return when (val event = inReplyToDetails.event()) { - is RepliedToEventDetails.Ready -> { + is EmbeddedEventDetails.Ready -> { InReplyTo.Ready( eventId = inReplyToId, content = timelineEventContentMapper.map(event.content), @@ -29,14 +29,14 @@ class InReplyToMapper( senderProfile = event.senderProfile.map(), ) } - is RepliedToEventDetails.Error -> InReplyTo.Error( + is EmbeddedEventDetails.Error -> InReplyTo.Error( eventId = inReplyToId, message = event.message, ) - RepliedToEventDetails.Pending -> InReplyTo.Pending( + EmbeddedEventDetails.Pending -> InReplyTo.Pending( eventId = inReplyToId, ) - is RepliedToEventDetails.Unavailable -> InReplyTo.NotLoaded( + is EmbeddedEventDetails.Unavailable -> InReplyTo.NotLoaded( eventId = inReplyToId ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt index e714d511bf..12e72eac0b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.matrix.impl.verification import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.verification.SessionVerificationData import io.element.android.libraries.matrix.api.verification.SessionVerificationService @@ -100,7 +101,6 @@ class RustSessionVerificationService( init { // Instantiate the verification controller when possible, this is needed to get incoming verification requests sessionCoroutineScope.launch { - // Needed to avoid crashes on unit tests due to the Rust SDK not being available tryOrNull { encryptionService.waitForE2eeInitializationTasks() initVerificationControllerIfNeeded() @@ -152,7 +152,7 @@ class RustSessionVerificationService( } private suspend fun tryOrFail(block: suspend () -> Unit) { - runCatching { + runCatchingExceptions { // Ensure the block cannot be cancelled, else if the Rust SDK emit a new state during the API execution, // the state machine may cancel the api call. withContext(NonCancellable) { @@ -184,7 +184,7 @@ class RustSessionVerificationService( sessionCoroutineScope.launch { // Ideally this should be `verificationController?.isVerified().orFalse()` but for some reason it returns false if run immediately // It also sometimes unexpectedly fails to report the session as verified, so we have to handle that possibility and fail if needed - runCatching { + runCatchingExceptions { withTimeout(20.seconds) { // Wait until the SDK reports the state as verified sessionVerifiedStatus.first { it == SessionVerifiedStatus.Verified } @@ -252,7 +252,7 @@ class RustSessionVerificationService( } private fun updateVerificationStatus() { - runCatching { + runCatchingExceptions { _sessionVerifiedStatus.value = encryptionService.verificationState().map() Timber.d("New verification status: ${_sessionVerifiedStatus.value}") } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt index 77152776aa..eed81c2dcb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/widget/DefaultCallWidgetSettingsProvider.kt @@ -49,7 +49,7 @@ class DefaultCallWidgetSettingsProvider @Inject constructor( sentryEnvironment = if (buildMeta.buildType == BuildType.RELEASE) "RELEASE" else "DEBUG", parentUrl = null, hideHeader = true, - controlledMediaDevices = false, + controlledMediaDevices = true, ) val rustWidgetSettings = newVirtualElementCallWidget(options) return MatrixWidgetSettings.fromRustWidgetSettings(rustWidgetSettings) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/FakeClientBuilderProvider.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/FakeClientBuilderProvider.kt index 4da0122bc8..3406b6686a 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/FakeClientBuilderProvider.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/FakeClientBuilderProvider.kt @@ -7,11 +7,11 @@ package io.element.android.libraries.matrix.impl -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustClientBuilder +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClientBuilder import org.matrix.rustcomponents.sdk.ClientBuilder class FakeClientBuilderProvider : ClientBuilderProvider { override fun provide(): ClientBuilder { - return FakeRustClientBuilder() + return FakeFfiClientBuilder() } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt index f9a1afb656..91d0f06d1c 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt @@ -9,8 +9,8 @@ package io.element.android.libraries.matrix.impl import com.google.common.truth.Truth.assertThat import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustClient -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustSyncService +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiSyncService import io.element.android.libraries.matrix.impl.room.FakeTimelineEventTypeFilterFactory import io.element.android.libraries.matrix.test.A_DEVICE_ID import io.element.android.libraries.matrix.test.A_USER_ID @@ -40,7 +40,7 @@ class RustMatrixClientTest { val clearCachesResult = lambdaRecorder { } val closeResult = lambdaRecorder { } val client = createRustMatrixClient( - client = FakeRustClient( + client = FakeFfiClient( clearCachesResult = clearCachesResult, closeResult = closeResult, ) @@ -52,7 +52,7 @@ class RustMatrixClientTest { } private fun TestScope.createRustMatrixClient( - client: Client = FakeRustClient(), + client: Client = FakeFfiClient(), sessionStore: SessionStore = InMemorySessionStore(), ) = RustMatrixClient( innerClient = client, @@ -62,7 +62,7 @@ class RustMatrixClientTest { sessionDelegate = aRustClientSessionDelegate( sessionStore = sessionStore, ), - innerSyncService = FakeRustSyncService(), + innerSyncService = FakeFfiSyncService(), dispatchers = testCoroutineDispatchers(), baseCacheDirectory = File(""), clock = FakeSystemClock(), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetailsKtTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetailsKtTest.kt index 69ec6e3f92..eb3ac75cf6 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetailsKtTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/HomeserverDetailsKtTest.kt @@ -9,14 +9,14 @@ package io.element.android.libraries.matrix.impl.auth import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustHomeserverLoginDetails +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiHomeserverLoginDetails import org.junit.Test class HomeserverDetailsKtTest { @Test fun `map should be correct`() { // Given - val homeserverLoginDetails = FakeRustHomeserverLoginDetails( + val homeserverLoginDetails = FakeFfiHomeserverLoginDetails( url = "https://example.org", supportsPasswordLogin = true, supportsOidcLogin = false diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginDataTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginDataTest.kt index c63a1bc2b1..1ba998fc41 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginDataTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/qrlogin/SdkQrCodeLoginDataTest.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.matrix.impl.auth.qrlogin import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeQrCodeData +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiQrCodeData import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import org.junit.Test @@ -16,7 +16,7 @@ class SdkQrCodeLoginDataTest { @Test fun `getServer reads the value from the Rust side, null case`() { val sut = SdkQrCodeLoginData( - rustQrCodeData = FakeQrCodeData( + rustQrCodeData = FakeFfiQrCodeData( serverNameResult = { null }, ), ) @@ -26,7 +26,7 @@ class SdkQrCodeLoginDataTest { @Test fun `getServer reads the value from the Rust side`() { val sut = SdkQrCodeLoginData( - rustQrCodeData = FakeQrCodeData( + rustQrCodeData = FakeFfiQrCodeData( serverNameResult = { A_HOMESERVER_URL }, ), ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt index 3b59ad903a..fbbb6c3375 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItem.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.matrix.impl.fixtures.factories -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustLazyTimelineItemProvider +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiLazyTimelineItemProvider import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_USER_ID import org.matrix.rustcomponents.sdk.EventOrTransactionId @@ -50,7 +50,7 @@ fun aRustEventTimelineItem( readReceipts = readReceipts, origin = origin, localCreatedAt = localCreatedAt, - lazyProvider = FakeRustLazyTimelineItemProvider( + lazyProvider = FakeFfiLazyTimelineItemProvider( debugInfo = debugInfo, shieldsState = shieldsState, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/NotificationItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/NotificationItem.kt index 7df10e623c..1c966cc047 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/NotificationItem.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/NotificationItem.kt @@ -8,7 +8,7 @@ package io.element.android.libraries.matrix.impl.fixtures.factories import io.element.android.libraries.matrix.api.core.ThreadId -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineEvent +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEvent import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_USER_NAME import org.matrix.rustcomponents.sdk.JoinRule @@ -65,7 +65,7 @@ fun aRustNotificationRoomInfo( ) fun aRustNotificationEventTimeline( - event: TimelineEvent = FakeRustTimelineEvent(), + event: TimelineEvent = FakeFfiTimelineEvent(), ) = NotificationEvent.Timeline( event = event, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt similarity index 74% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClient.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt index 583e397daa..5c299f1adf 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClient.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt @@ -15,6 +15,7 @@ import io.element.android.tests.testutils.simulateLongTask import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.Encryption +import org.matrix.rustcomponents.sdk.IgnoredUsersListener import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.NotificationClient import org.matrix.rustcomponents.sdk.NotificationProcessSetup @@ -23,16 +24,18 @@ import org.matrix.rustcomponents.sdk.PusherIdentifiers import org.matrix.rustcomponents.sdk.PusherKind import org.matrix.rustcomponents.sdk.RoomDirectorySearch import org.matrix.rustcomponents.sdk.Session +import org.matrix.rustcomponents.sdk.SessionVerificationController import org.matrix.rustcomponents.sdk.SyncServiceBuilder import org.matrix.rustcomponents.sdk.TaskHandle import org.matrix.rustcomponents.sdk.UnableToDecryptDelegate +import org.matrix.rustcomponents.sdk.UserProfile -class FakeRustClient( +class FakeFfiClient( private val userId: String = A_USER_ID.value, private val deviceId: String = A_DEVICE_ID.value, - private val notificationClient: NotificationClient = FakeRustNotificationClient(), - private val notificationSettings: NotificationSettings = FakeRustNotificationSettings(), - private val encryption: Encryption = FakeRustEncryption(), + private val notificationClient: NotificationClient = FakeFfiNotificationClient(), + private val notificationSettings: NotificationSettings = FakeFfiNotificationSettings(), + private val encryption: Encryption = FakeFfiEncryption(), private val session: Session = aRustSession(), private val clearCachesResult: () -> Unit = { lambdaError() }, private val withUtdHook: (UnableToDecryptDelegate) -> Unit = { lambdaError() }, @@ -44,11 +47,11 @@ class FakeRustClient( override suspend fun getNotificationSettings(): NotificationSettings = notificationSettings override fun encryption(): Encryption = encryption override fun session(): Session = session - override fun setDelegate(delegate: ClientDelegate?): TaskHandle = FakeRustTaskHandle() + override fun setDelegate(delegate: ClientDelegate?): TaskHandle = FakeFfiTaskHandle() override suspend fun cachedAvatarUrl(): String? = null override suspend fun restoreSession(session: Session) = Unit - override fun syncService(): SyncServiceBuilder = FakeRustSyncServiceBuilder() - override fun roomDirectorySearch(): RoomDirectorySearch = FakeRustRoomDirectorySearch() + override fun syncService(): SyncServiceBuilder = FakeFfiSyncServiceBuilder() + override fun roomDirectorySearch(): RoomDirectorySearch = FakeFfiRoomDirectorySearch() override suspend fun setPusher( identifiers: PusherIdentifiers, kind: PusherKind, @@ -61,5 +64,16 @@ class FakeRustClient( override suspend fun deletePusher(identifiers: PusherIdentifiers) = Unit override suspend fun clearCaches() = simulateLongTask { clearCachesResult() } override suspend fun setUtdDelegate(utdDelegate: UnableToDecryptDelegate) = withUtdHook(utdDelegate) + override suspend fun getSessionVerificationController(): SessionVerificationController = FakeFfiSessionVerificationController() + override suspend fun ignoredUsers(): List { + return emptyList() + } + override fun subscribeToIgnoredUsers(listener: IgnoredUsersListener): TaskHandle { + return FakeFfiTaskHandle() + } + + override suspend fun getProfile(userId: String): UserProfile { + return UserProfile(userId = userId, displayName = null, avatarUrl = null) + } override fun close() = closeResult() } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClientBuilder.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClientBuilder.kt similarity index 94% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClientBuilder.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClientBuilder.kt index eed26ba2dc..9f64487448 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustClientBuilder.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClientBuilder.kt @@ -20,7 +20,7 @@ import uniffi.matrix_sdk.BackupDownloadStrategy import uniffi.matrix_sdk_crypto.CollectStrategy import uniffi.matrix_sdk_crypto.TrustRequirement -class FakeRustClientBuilder : ClientBuilder(NoPointer) { +class FakeFfiClientBuilder : ClientBuilder(NoPointer) { override fun addRootCertificates(certificates: List) = this override fun autoEnableBackups(autoEnableBackups: Boolean) = this override fun autoEnableCrossSigning(autoEnableCrossSigning: Boolean) = this @@ -43,10 +43,10 @@ class FakeRustClientBuilder : ClientBuilder(NoPointer) { override fun username(username: String) = this override suspend fun buildWithQrCode(qrCodeData: QrCodeData, oidcConfiguration: OidcConfiguration, progressListener: QrLoginProgressListener): Client { - return FakeRustClient() + return FakeFfiClient() } override suspend fun build(): Client { - return FakeRustClient(withUtdHook = {}) + return FakeFfiClient(withUtdHook = {}) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiEncryption.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiEncryption.kt new file mode 100644 index 0000000000..536288caef --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiEncryption.kt @@ -0,0 +1,46 @@ +/* + * 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.matrix.impl.fixtures.fakes + +import io.element.android.tests.testutils.simulateLongTask +import org.matrix.rustcomponents.sdk.BackupState +import org.matrix.rustcomponents.sdk.BackupStateListener +import org.matrix.rustcomponents.sdk.Encryption +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.RecoveryState +import org.matrix.rustcomponents.sdk.RecoveryStateListener +import org.matrix.rustcomponents.sdk.TaskHandle +import org.matrix.rustcomponents.sdk.VerificationStateListener + +class FakeFfiEncryption : Encryption(NoPointer) { + override fun verificationStateListener(listener: VerificationStateListener): TaskHandle { + return FakeFfiTaskHandle() + } + + override fun recoveryStateListener(listener: RecoveryStateListener): TaskHandle { + return FakeFfiTaskHandle() + } + + override suspend fun waitForE2eeInitializationTasks() = simulateLongTask {} + + override suspend fun isLastDevice(): Boolean { + return false + } + + override fun backupState(): BackupState { + return BackupState.ENABLED + } + + override fun recoveryState(): RecoveryState { + return RecoveryState.ENABLED + } + + override fun backupStateListener(listener: BackupStateListener): TaskHandle { + return FakeFfiTaskHandle() + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustHomeserverLoginDetails.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverLoginDetails.kt similarity index 95% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustHomeserverLoginDetails.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverLoginDetails.kt index 2cfca5f1b5..8977470365 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustHomeserverLoginDetails.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiHomeserverLoginDetails.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes import org.matrix.rustcomponents.sdk.HomeserverLoginDetails import org.matrix.rustcomponents.sdk.NoPointer -class FakeRustHomeserverLoginDetails( +class FakeFfiHomeserverLoginDetails( private val url: String = "https://example.org", private val supportsPasswordLogin: Boolean = true, private val supportsOidcLogin: Boolean = false diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustLazyTimelineItemProvider.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLazyTimelineItemProvider.kt similarity index 95% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustLazyTimelineItemProvider.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLazyTimelineItemProvider.kt index c6c4a7aa72..6149a9164d 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustLazyTimelineItemProvider.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiLazyTimelineItemProvider.kt @@ -14,7 +14,7 @@ import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.SendHandle import org.matrix.rustcomponents.sdk.ShieldState -class FakeRustLazyTimelineItemProvider( +class FakeFfiLazyTimelineItemProvider( private val debugInfo: EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo(), private val shieldsState: ShieldState? = null, ) : LazyTimelineItemProvider(NoPointer) { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustNotificationClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationClient.kt similarity index 95% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustNotificationClient.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationClient.kt index 802f20c509..ec244ede74 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustNotificationClient.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationClient.kt @@ -12,7 +12,7 @@ import org.matrix.rustcomponents.sdk.NotificationClient import org.matrix.rustcomponents.sdk.NotificationItem import org.matrix.rustcomponents.sdk.NotificationItemsRequest -class FakeRustNotificationClient( +class FakeFfiNotificationClient( var notificationItemResult: Map = emptyMap(), ) : NotificationClient(NoPointer) { override suspend fun getNotifications(requests: List): Map { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustNotificationSettings.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationSettings.kt similarity index 96% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustNotificationSettings.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationSettings.kt index 3a96102cb6..7a65b8d2cd 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustNotificationSettings.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationSettings.kt @@ -13,7 +13,7 @@ import org.matrix.rustcomponents.sdk.NotificationSettings import org.matrix.rustcomponents.sdk.NotificationSettingsDelegate import org.matrix.rustcomponents.sdk.RoomNotificationSettings -class FakeRustNotificationSettings( +class FakeFfiNotificationSettings( private val roomNotificationSettings: RoomNotificationSettings = aRustRoomNotificationSettings(), ) : NotificationSettings(NoPointer) { private var delegate: NotificationSettingsDelegate? = null diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeQrCodeData.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiQrCodeData.kt similarity index 95% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeQrCodeData.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiQrCodeData.kt index 7033d1de9a..1be8b87a66 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeQrCodeData.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiQrCodeData.kt @@ -11,7 +11,7 @@ import io.element.android.tests.testutils.lambda.lambdaError import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.QrCodeData -class FakeQrCodeData( +class FakeFfiQrCodeData( private val serverNameResult: () -> String? = { lambdaError() }, ) : QrCodeData(NoPointer) { override fun serverName(): String? { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoom.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoom.kt similarity index 98% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoom.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoom.kt index e1652381ff..45aa970658 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoom.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoom.kt @@ -17,7 +17,7 @@ import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomMembersIterator -class FakeRustRoom( +class FakeFfiRoom( private val roomId: RoomId = A_ROOM_ID, private val getMembers: () -> RoomMembersIterator = { lambdaError() }, private val getMembersNoSync: () -> RoomMembersIterator = { lambdaError() }, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomDirectorySearch.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomDirectorySearch.kt similarity index 94% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomDirectorySearch.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomDirectorySearch.kt index 0b2fff6417..b090262e31 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomDirectorySearch.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomDirectorySearch.kt @@ -14,7 +14,7 @@ import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntriesListener import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate import org.matrix.rustcomponents.sdk.TaskHandle -class FakeRustRoomDirectorySearch( +class FakeFfiRoomDirectorySearch( var isAtLastPage: Boolean = false, ) : RoomDirectorySearch(NoPointer) { override suspend fun isAtLastPage(): Boolean { @@ -28,7 +28,7 @@ class FakeRustRoomDirectorySearch( override suspend fun results(listener: RoomDirectorySearchEntriesListener): TaskHandle { this.listener = listener - return FakeRustTaskHandle() + return FakeFfiTaskHandle() } fun emitResult(roomEntriesUpdate: List) { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomList.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomList.kt similarity index 88% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomList.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomList.kt index e86e5fd27f..d51a742368 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomList.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomList.kt @@ -10,4 +10,4 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.RoomList -class FakeRustRoomList : RoomList(NoPointer) +class FakeFfiRoomList : RoomList(NoPointer) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomListService.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomListService.kt similarity index 77% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomListService.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomListService.kt index 1251bd514f..604f5289a0 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomListService.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomListService.kt @@ -10,13 +10,14 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.RoomList import org.matrix.rustcomponents.sdk.RoomListService +import org.matrix.rustcomponents.sdk.RoomListServiceStateListener import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicatorListener import org.matrix.rustcomponents.sdk.TaskHandle -class FakeRustRoomListService : RoomListService(NoPointer) { +class FakeFfiRoomListService : RoomListService(NoPointer) { override suspend fun allRooms(): RoomList { - return FakeRustRoomList() + return FakeFfiRoomList() } private var listener: RoomListServiceSyncIndicatorListener? = null @@ -26,10 +27,14 @@ class FakeRustRoomListService : RoomListService(NoPointer) { listener: RoomListServiceSyncIndicatorListener, ): TaskHandle { this.listener = listener - return FakeRustTaskHandle() + return FakeFfiTaskHandle() } fun emitRoomListServiceSyncIndicator(syncIndicator: RoomListServiceSyncIndicator) { listener?.onUpdate(syncIndicator) } + + override fun state(listener: RoomListServiceStateListener): TaskHandle { + return FakeFfiTaskHandle() + } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomMembersIterator.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomMembersIterator.kt similarity index 96% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomMembersIterator.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomMembersIterator.kt index 2ef8ba56db..28ee4791e5 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustRoomMembersIterator.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiRoomMembersIterator.kt @@ -11,7 +11,7 @@ import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMembersIterator -class FakeRustRoomMembersIterator( +class FakeFfiRoomMembersIterator( private var members: List? = null ) : RoomMembersIterator(NoPointer) { override fun len(): UInt { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSessionVerificationController.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSessionVerificationController.kt new file mode 100644 index 0000000000..2ff1e1d6ac --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSessionVerificationController.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.SessionVerificationController +import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate + +class FakeFfiSessionVerificationController : SessionVerificationController(NoPointer) { + override fun setDelegate(delegate: SessionVerificationControllerDelegate?) {} +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustSyncService.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSyncService.kt similarity index 58% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustSyncService.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSyncService.kt index 734968beda..5d32139c8e 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustSyncService.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSyncService.kt @@ -10,9 +10,15 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.RoomListService import org.matrix.rustcomponents.sdk.SyncService +import org.matrix.rustcomponents.sdk.SyncServiceStateObserver +import org.matrix.rustcomponents.sdk.TaskHandle -class FakeRustSyncService( - private val roomListService: RoomListService = FakeRustRoomListService(), +class FakeFfiSyncService( + private val roomListService: RoomListService = FakeFfiRoomListService(), ) : SyncService(NoPointer) { override fun roomListService(): RoomListService = roomListService + override fun state(listener: SyncServiceStateObserver): TaskHandle { + return FakeFfiTaskHandle() + } + override suspend fun stop() {} } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustSyncServiceBuilder.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSyncServiceBuilder.kt similarity index 77% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustSyncServiceBuilder.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSyncServiceBuilder.kt index e36f6f0b1f..93dab14a0c 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustSyncServiceBuilder.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiSyncServiceBuilder.kt @@ -11,7 +11,7 @@ import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.SyncService import org.matrix.rustcomponents.sdk.SyncServiceBuilder -class FakeRustSyncServiceBuilder : SyncServiceBuilder(NoPointer) { +class FakeFfiSyncServiceBuilder : SyncServiceBuilder(NoPointer) { override fun withOfflineMode(): SyncServiceBuilder = this - override suspend fun finish(): SyncService = FakeRustSyncService() + override suspend fun finish(): SyncService = FakeFfiSyncService() } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTaskHandle.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTaskHandle.kt similarity index 89% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTaskHandle.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTaskHandle.kt index 7aaefb2fd8..66c51017df 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTaskHandle.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTaskHandle.kt @@ -10,7 +10,7 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.TaskHandle -class FakeRustTaskHandle : TaskHandle(NoPointer) { +class FakeFfiTaskHandle : TaskHandle(NoPointer) { override fun cancel() = Unit override fun destroy() = Unit } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimeline.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimeline.kt similarity index 85% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimeline.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimeline.kt index 6fa8e75704..09d79f8790 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimeline.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimeline.kt @@ -15,11 +15,11 @@ import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineListener import uniffi.matrix_sdk.RoomPaginationStatus -class FakeRustTimeline : Timeline(NoPointer) { +class FakeFfiTimeline : Timeline(NoPointer) { private var listener: TimelineListener? = null override suspend fun addListener(listener: TimelineListener): TaskHandle { this.listener = listener - return FakeRustTaskHandle() + return FakeFfiTaskHandle() } fun emitDiff(diff: List) { @@ -29,12 +29,16 @@ class FakeRustTimeline : Timeline(NoPointer) { private var paginationStatusListener: PaginationStatusListener? = null override suspend fun subscribeToBackPaginationStatus(listener: PaginationStatusListener): TaskHandle { this.paginationStatusListener = listener - return FakeRustTaskHandle() + return FakeFfiTaskHandle() } fun emitPaginationStatus(status: RoomPaginationStatus) { paginationStatusListener!!.onUpdate(status) } + override suspend fun paginateBackwards(numEvents: UShort): Boolean { + return true + } + override suspend fun fetchMembers() = Unit } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineDiff.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineDiff.kt similarity index 92% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineDiff.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineDiff.kt index f3d0fb0efa..605d2d6815 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineDiff.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineDiff.kt @@ -14,9 +14,9 @@ import org.matrix.rustcomponents.sdk.TimelineChange import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineItem -class FakeRustTimelineDiff( +class FakeFfiTimelineDiff( private val change: TimelineChange, - private val item: TimelineItem? = FakeRustTimelineItem() + private val item: TimelineItem? = FakeFfiTimelineItem() ) : TimelineDiff(NoPointer) { override fun change() = change override fun append(): List? = item?.let { listOf(it) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineEvent.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineEvent.kt similarity index 97% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineEvent.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineEvent.kt index 3b70fb2670..e514cef7a8 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineEvent.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineEvent.kt @@ -14,7 +14,7 @@ import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.TimelineEvent import org.matrix.rustcomponents.sdk.TimelineEventType -class FakeRustTimelineEvent( +class FakeFfiTimelineEvent( val timestamp: ULong = A_FAKE_TIMESTAMP.toULong(), val timelineEventType: TimelineEventType = aRustTimelineEventTypeMessageLike(), val senderId: String = A_USER_ID_2.value, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineEventTypeFilter.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineEventTypeFilter.kt similarity index 82% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineEventTypeFilter.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineEventTypeFilter.kt index dae1eb366c..dcab0bba6e 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineEventTypeFilter.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineEventTypeFilter.kt @@ -10,4 +10,4 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter -class FakeRustTimelineEventTypeFilter : TimelineEventTypeFilter(NoPointer) +class FakeFfiTimelineEventTypeFilter : TimelineEventTypeFilter(NoPointer) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineItem.kt similarity index 96% rename from libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItem.kt rename to libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineItem.kt index e6f1272fb7..a3f7a18c21 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItem.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiTimelineItem.kt @@ -13,7 +13,7 @@ import org.matrix.rustcomponents.sdk.TimelineItem import org.matrix.rustcomponents.sdk.TimelineUniqueId import org.matrix.rustcomponents.sdk.VirtualTimelineItem -class FakeRustTimelineItem( +class FakeFfiTimelineItem( private val asEventResult: EventTimelineItem? = null, ) : TimelineItem(NoPointer) { override fun asEvent(): EventTimelineItem? = asEventResult diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustEncryption.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustEncryption.kt deleted file mode 100644 index c966025139..0000000000 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustEncryption.kt +++ /dev/null @@ -1,24 +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.matrix.impl.fixtures.fakes - -import org.matrix.rustcomponents.sdk.Encryption -import org.matrix.rustcomponents.sdk.NoPointer -import org.matrix.rustcomponents.sdk.RecoveryStateListener -import org.matrix.rustcomponents.sdk.TaskHandle -import org.matrix.rustcomponents.sdk.VerificationStateListener - -class FakeRustEncryption : Encryption(NoPointer) { - override fun verificationStateListener(listener: VerificationStateListener): TaskHandle { - return FakeRustTaskHandle() - } - - override fun recoveryStateListener(listener: RecoveryStateListener): TaskHandle { - return FakeRustTaskHandle() - } -} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationServiceTest.kt index c8d40d7332..c0d7ddf09b 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationServiceTest.kt @@ -11,7 +11,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.impl.fixtures.factories.aRustNotificationItem -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustNotificationClient +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiNotificationClient import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -28,7 +28,7 @@ import org.matrix.rustcomponents.sdk.NotificationClient class RustNotificationServiceTest { @Test fun test() = runTest { - val notificationClient = FakeRustNotificationClient( + val notificationClient = FakeFfiNotificationClient( notificationItemResult = mapOf(AN_EVENT_ID.value to aRustNotificationItem()), ) val sut = createRustNotificationService( @@ -48,7 +48,7 @@ class RustNotificationServiceTest { } private fun TestScope.createRustNotificationService( - notificationClient: NotificationClient = FakeRustNotificationClient(), + notificationClient: NotificationClient = FakeFfiNotificationClient(), clock: SystemClock = FakeSystemClock(), ) = RustNotificationService( diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsServiceTest.kt index 34b8c114b6..d2dd425132 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notificationsettings/RustNotificationSettingsServiceTest.kt @@ -9,8 +9,8 @@ package io.element.android.libraries.matrix.impl.notificationsettings import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.room.RoomNotificationMode -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustClient -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustNotificationSettings +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiNotificationSettings import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope @@ -32,9 +32,9 @@ class RustNotificationSettingsServiceTest { } private fun TestScope.createRustNotificationSettingsService( - notificationSettings: NotificationSettings = FakeRustNotificationSettings(), + notificationSettings: NotificationSettings = FakeFfiNotificationSettings(), ) = RustNotificationSettingsService( - client = FakeRustClient( + client = FakeFfiClient( notificationSettings = notificationSettings, ), sessionCoroutineScope = this, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersServiceTest.kt index bedfe17281..22e015a27c 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersServiceTest.kt @@ -9,7 +9,7 @@ package io.element.android.libraries.matrix.impl.pushers import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustClient +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.runTest import org.junit.Test @@ -18,7 +18,7 @@ class RustPushersServiceTest { @Test fun `setPusher should invoke the client method`() = runTest { val sut = RustPushersService( - client = FakeRustClient(), + client = FakeFfiClient(), dispatchers = testCoroutineDispatchers() ) sut.setHttpPusher( @@ -29,7 +29,7 @@ class RustPushersServiceTest { @Test fun `unsetPusher should invoke the client method`() = runTest { val sut = RustPushersService( - client = FakeRustClient(), + client = FakeFfiClient(), dispatchers = testCoroutineDispatchers() ) sut.unsetHttpPusher( diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/FakeTimelineEventTypeFilterFactory.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/FakeTimelineEventTypeFilterFactory.kt index abb6004ac4..d7d800ac23 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/FakeTimelineEventTypeFilterFactory.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/FakeTimelineEventTypeFilterFactory.kt @@ -8,11 +8,11 @@ package io.element.android.libraries.matrix.impl.room import io.element.android.libraries.matrix.api.room.StateEventType -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineEventTypeFilter +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineEventTypeFilter import org.matrix.rustcomponents.sdk.TimelineEventTypeFilter class FakeTimelineEventTypeFilterFactory : TimelineEventTypeFilterFactory { override fun create(listStateEventType: List): TimelineEventTypeFilter { - return FakeRustTimelineEventTypeFilter() + return FakeFfiTimelineEventTypeFilter() } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt index 6b2105eb2a..f25f568c5a 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/RustBaseRoomTest.kt @@ -14,8 +14,8 @@ 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.RoomMembershipObserver import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoom +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService import io.element.android.libraries.matrix.test.A_DEVICE_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.room.aRoomInfo @@ -41,7 +41,7 @@ class RustBaseRoomTest { val roomMembershipObserver = RoomMembershipObserver() val rustBaseRoom = createRustBaseRoom( initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.JOINED), - innerRoom = FakeRustRoom( + innerRoom = FakeFfiRoom( leaveLambda = { // Simulate a successful leave } @@ -61,7 +61,7 @@ class RustBaseRoomTest { val roomMembershipObserver = RoomMembershipObserver() val rustBaseRoom = createRustBaseRoom( initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.KNOCKED), - innerRoom = FakeRustRoom( + innerRoom = FakeFfiRoom( leaveLambda = { // Simulate a successful leave } @@ -81,7 +81,7 @@ class RustBaseRoomTest { val roomMembershipObserver = RoomMembershipObserver() val rustBaseRoom = createRustBaseRoom( initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED), - innerRoom = FakeRustRoom( + innerRoom = FakeFfiRoom( leaveLambda = { // Simulate a successful leave } @@ -101,7 +101,7 @@ class RustBaseRoomTest { val roomMembershipObserver = RoomMembershipObserver() val rustBaseRoom = createRustBaseRoom( initialRoomInfo = aRoomInfo(currentUserMembership = CurrentUserMembership.INVITED), - innerRoom = FakeRustRoom( + innerRoom = FakeFfiRoom( leaveLambda = { error("Leave failed") } ), roomMembershipObserver = roomMembershipObserver, @@ -127,7 +127,7 @@ class RustBaseRoomTest { private fun TestScope.createRustBaseRoom( initialRoomInfo: RoomInfo = aRoomInfo(), - innerRoom: FakeRustRoom = FakeRustRoom(), + innerRoom: FakeFfiRoom = FakeFfiRoom(), roomMembershipObserver: RoomMembershipObserver = RoomMembershipObserver(), ): RustBaseRoom { val dispatchers = testCoroutineDispatchers() @@ -137,7 +137,7 @@ class RustBaseRoomTest { innerRoom = innerRoom, coroutineDispatchers = dispatchers, roomSyncSubscriber = RoomSyncSubscriber( - roomListService = FakeRustRoomListService(), + roomListService = FakeFfiRoomListService(), dispatchers = dispatchers, ), roomMembershipObserver = roomMembershipObserver, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt index 6193d229c8..d33148931e 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt @@ -20,7 +20,6 @@ import io.element.android.libraries.matrix.test.A_SERVER_LIST import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.aRoomInfo -import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -30,9 +29,9 @@ import org.junit.Test class DefaultJoinRoomTest { @Test fun `when using roomId and there is no server names, the classic join room API is used`() = runTest { - val roomSummary = aRoomSummary() - val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } - val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomSummary) } + val roomInfo = aRoomInfo() + val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomInfo) } + val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomInfo) } val roomResult = FakeBaseRoom().apply { givenRoomInfo(aRoomInfo()) } @@ -67,9 +66,9 @@ class DefaultJoinRoomTest { @Test fun `when using roomId and server names are available, joinRoomByIdOrAlias API is used`() = runTest { - val roomSummary = aRoomSummary() - val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } - val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomSummary) } + val roomInfo = aRoomInfo() + val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomInfo) } + val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomInfo) } val roomResult = FakeBaseRoom().apply { givenRoomInfo(aRoomInfo()) } @@ -105,9 +104,9 @@ class DefaultJoinRoomTest { @Test fun `when using roomAlias, joinRoomByIdOrAlias API is used`() = runTest { - val roomSummary = aRoomSummary() - val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomSummary) } - val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomSummary) } + val roomInfo = aRoomInfo() + val joinRoomLambda = lambdaRecorder { _: RoomId -> Result.success(roomInfo) } + val joinRoomByIdOrAliasLambda = lambdaRecorder { _: RoomIdOrAlias, _: List -> Result.success(roomInfo) } val roomResult = FakeBaseRoom().apply { givenRoomInfo(aRoomInfo()) } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt index 94f786b581..b11a3a6cd5 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/member/RoomMemberListFetcherTest.kt @@ -12,8 +12,8 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.room.RoomMembersState import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomMember -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomMembersIterator +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoom +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomMembersIterator import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.CACHE import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.CACHE_AND_SERVER import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher.Source.SERVER @@ -28,8 +28,8 @@ import org.junit.Test class RoomMemberListFetcherTest { @Test fun `fetchRoomMembers with CACHE source - emits cached members, if any`() = runTest { - val room = FakeRustRoom(getMembersNoSync = { - FakeRustRoomMembersIterator( + val room = FakeFfiRoom(getMembersNoSync = { + FakeFfiRoomMembersIterator( listOf( aRustRoomMember(A_USER_ID), aRustRoomMember(A_USER_ID_2), @@ -55,8 +55,8 @@ class RoomMemberListFetcherTest { @Test fun `fetchRoomMembers with CACHE source - emits empty list, if no members exist`() = runTest { - val room = FakeRustRoom(getMembersNoSync = { - FakeRustRoomMembersIterator(emptyList()) + val room = FakeFfiRoom(getMembersNoSync = { + FakeFfiRoomMembersIterator(emptyList()) }) val fetcher = RoomMemberListFetcher(room, Dispatchers.Default) @@ -70,7 +70,7 @@ class RoomMemberListFetcherTest { @Test fun `fetchRoomMembers with CACHE source - emits Error on error found`() = runTest { - val room = FakeRustRoom(getMembersNoSync = { + val room = FakeFfiRoom(getMembersNoSync = { error("Some unexpected issue") }) @@ -85,8 +85,8 @@ class RoomMemberListFetcherTest { @Test fun `fetchRoomMembers with CACHE source - emits all items at once`() = runTest { - val room = FakeRustRoom(getMembersNoSync = { - FakeRustRoomMembersIterator( + val room = FakeFfiRoom(getMembersNoSync = { + FakeFfiRoomMembersIterator( listOf( aRustRoomMember(A_USER_ID), aRustRoomMember(A_USER_ID_2), @@ -112,8 +112,8 @@ class RoomMemberListFetcherTest { @Test fun `fetchRoomMembers with SERVER source - emits only new members, if any`() = runTest { - val room = FakeRustRoom(getMembers = { - FakeRustRoomMembersIterator( + val room = FakeFfiRoom(getMembers = { + FakeFfiRoomMembersIterator( listOf( aRustRoomMember(A_USER_ID), aRustRoomMember(A_USER_ID_2), @@ -134,7 +134,7 @@ class RoomMemberListFetcherTest { @Test fun `fetchRoomMembers with SERVER source - on error it emits an Error item`() = runTest { - val room = FakeRustRoom(getMembers = { error("An unexpected error") }) + val room = FakeFfiRoom(getMembers = { error("An unexpected error") }) val fetcher = RoomMemberListFetcher(room, Dispatchers.Default) fetcher.membersFlow.test { @@ -148,12 +148,12 @@ class RoomMemberListFetcherTest { @Test fun `fetchRoomMembers with CACHE_AND_SERVER source - returns cached items first, then new ones`() = runTest { - val room = FakeRustRoom( + val room = FakeFfiRoom( getMembersNoSync = { - FakeRustRoomMembersIterator(listOf(aRustRoomMember(A_USER_ID_4))) + FakeFfiRoomMembersIterator(listOf(aRustRoomMember(A_USER_ID_4))) }, getMembers = { - FakeRustRoomMembersIterator( + FakeFfiRoomMembersIterator( listOf( aRustRoomMember(A_USER_ID), aRustRoomMember(A_USER_ID_2), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustBaseRoomDirectoryListTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustBaseRoomDirectoryListTest.kt index f7f0b3cae7..460ef37e99 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustBaseRoomDirectoryListTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustBaseRoomDirectoryListTest.kt @@ -11,7 +11,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomDescription -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomDirectorySearch +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomDirectorySearch import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -26,7 +26,7 @@ import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate class RustBaseRoomDirectoryListTest { @Test fun `check that the state emits the expected values`() = runTest { - val roomDirectorySearch = FakeRustRoomDirectorySearch() + val roomDirectorySearch = FakeFfiRoomDirectorySearch() val mapper = RoomDescriptionMapper() val sut = createRustRoomDirectoryList( roomDirectorySearch = roomDirectorySearch, @@ -78,7 +78,7 @@ class RustBaseRoomDirectoryListTest { } private fun TestScope.createRustRoomDirectoryList( - roomDirectorySearch: RoomDirectorySearch = FakeRustRoomDirectorySearch(), + roomDirectorySearch: RoomDirectorySearch = FakeFfiRoomDirectorySearch(), ) = RustRoomDirectoryList( inner = roomDirectorySearch, coroutineScope = backgroundScope, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustBaseRoomDirectoryServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustBaseRoomDirectoryServiceTest.kt index 36ff2efbfc..cc39e00c9f 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustBaseRoomDirectoryServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustBaseRoomDirectoryServiceTest.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.matrix.impl.roomdirectory -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustClient +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiClient import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Test @@ -15,7 +15,7 @@ import org.junit.Test class RustBaseRoomDirectoryServiceTest { @Test fun test() = runTest { - val client = FakeRustClient() + val client = FakeFfiClient() val sut = RustRoomDirectoryService( client = client, sessionDispatcher = StandardTestDispatcher(testScheduler), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactoryTest.kt index 683b218261..2d97f2c589 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactoryTest.kt @@ -7,8 +7,8 @@ package io.element.android.libraries.matrix.impl.roomlist -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomList -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomList +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService import kotlinx.coroutines.test.runTest import org.junit.Test import kotlin.coroutines.EmptyCoroutineContext @@ -17,14 +17,14 @@ class RoomListFactoryTest { @Test fun `createRoomList should work`() = runTest { val sut = RoomListFactory( - innerRoomListService = FakeRustRoomListService(), + innerRoomListService = FakeFfiRoomListService(), sessionCoroutineScope = backgroundScope, ) sut.createRoomList( pageSize = 10, coroutineContext = EmptyCoroutineContext, ) { - FakeRustRoomList() + FakeFfiRoomList() } } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTest.kt index 79bae7e37e..2b54463dea 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomSummaryListProcessorTest.kt @@ -10,8 +10,8 @@ package io.element.android.libraries.matrix.impl.roomlist import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.roomlist.RoomSummary -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoom -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoom +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_3 @@ -170,14 +170,14 @@ class RoomSummaryListProcessorTest { assertThat(summaries.value[index].roomId).isEqualTo(A_ROOM_ID_3) } - private fun aRustRoom(roomId: RoomId = A_ROOM_ID) = FakeRustRoom( + private fun aRustRoom(roomId: RoomId = A_ROOM_ID) = FakeFfiRoom( roomId = roomId, latestEventLambda = { null }, ) private fun TestScope.createProcessor() = RoomSummaryListProcessor( summaries, - FakeRustRoomListService(), + FakeFfiRoomListService(), coroutineContext = StandardTestDispatcher(testScheduler), roomSummaryDetailsFactory = RoomSummaryFactory(), ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustBaseRoomListServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustBaseRoomListServiceTest.kt index 24447d4d74..561c1f245f 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustBaseRoomListServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustBaseRoomListServiceTest.kt @@ -9,7 +9,7 @@ package io.element.android.libraries.matrix.impl.roomlist import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.roomlist.RoomListService -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -25,7 +25,7 @@ import org.matrix.rustcomponents.sdk.RoomListService as RustRoomListService class RustBaseRoomListServiceTest { @Test fun `syncIndicator should emit the expected values`() = runTest { - val roomListService = FakeRustRoomListService() + val roomListService = FakeFfiRoomListService() val sut = createRustRoomListService( roomListService = roomListService, ) @@ -42,7 +42,7 @@ class RustBaseRoomListServiceTest { } private fun TestScope.createRustRoomListService( - roomListService: RustRoomListService = FakeRustRoomListService(), + roomListService: RustRoomListService = FakeFfiRoomListService(), ) = RustRoomListService( innerRoomListService = roomListService, sessionDispatcher = StandardTestDispatcher(testScheduler), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt index 72f98db008..a4029417cf 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt @@ -9,7 +9,7 @@ package io.element.android.libraries.matrix.impl.timeline import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineDiff +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineDiff import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper @@ -33,7 +33,7 @@ class MatrixTimelineDiffProcessorTest { fun `Append adds new entries at the end of the list`() = runTest { timelineItems.value = listOf(anEvent) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.APPEND))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.APPEND))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( anEvent, @@ -45,7 +45,7 @@ class MatrixTimelineDiffProcessorTest { fun `PushBack adds a new entry at the end of the list`() = runTest { timelineItems.value = listOf(anEvent) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.PUSH_BACK))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.PUSH_BACK))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( anEvent, @@ -57,7 +57,7 @@ class MatrixTimelineDiffProcessorTest { fun `PushFront inserts a new entry at the start of the list`() = runTest { timelineItems.value = listOf(anEvent) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.PUSH_FRONT))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.PUSH_FRONT))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( MatrixTimelineItem.Other, @@ -69,7 +69,7 @@ class MatrixTimelineDiffProcessorTest { fun `Set replaces an entry at some index`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.SET))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.SET))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( anEvent, @@ -81,7 +81,7 @@ class MatrixTimelineDiffProcessorTest { fun `Insert inserts a new entry at the provided index`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.INSERT))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.INSERT))) assertThat(timelineItems.value.count()).isEqualTo(3) assertThat(timelineItems.value).containsExactly( anEvent, @@ -94,7 +94,7 @@ class MatrixTimelineDiffProcessorTest { fun `Remove removes an entry at some index`() = runTest { timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.REMOVE))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.REMOVE))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( anEvent, @@ -106,7 +106,7 @@ class MatrixTimelineDiffProcessorTest { fun `PopBack removes an entry at the end of the list`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.POP_BACK))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.POP_BACK))) assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value).containsExactly( anEvent, @@ -117,7 +117,7 @@ class MatrixTimelineDiffProcessorTest { fun `PopFront removes an entry at the start of the list`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.POP_FRONT))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.POP_FRONT))) assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value).containsExactly( anEvent2, @@ -128,7 +128,7 @@ class MatrixTimelineDiffProcessorTest { fun `Clear removes all the entries`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.CLEAR))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.CLEAR))) assertThat(timelineItems.value).isEmpty() } @@ -136,7 +136,7 @@ class MatrixTimelineDiffProcessorTest { fun `Truncate removes all entries after the provided length`() = runTest { timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.TRUNCATE))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.TRUNCATE))) assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value).containsExactly( anEvent, @@ -147,7 +147,7 @@ class MatrixTimelineDiffProcessorTest { fun `Reset removes all entries and add the provided ones`() = runTest { timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) val processor = createMatrixTimelineDiffProcessor(timelineItems) - processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.RESET))) + processor.postDiffs(listOf(FakeFfiTimelineDiff(change = TimelineChange.RESET))) assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value).containsExactly( MatrixTimelineItem.Other, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimelineTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimelineTest.kt index 1498b70468..56d1309b2a 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimelineTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/RustTimelineTest.kt @@ -16,9 +16,9 @@ import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimeline -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineDiff +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiRoomListService +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimeline +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineDiff import io.element.android.libraries.matrix.impl.room.RoomContentForwarder import io.element.android.libraries.matrix.test.room.FakeJoinedRoom import io.element.android.libraries.matrix.test.room.aRoomInfo @@ -40,7 +40,7 @@ import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline class RustTimelineTest { @Test fun `ensure that the timeline emits new loading item when pagination does not bring new events`() = runTest { - val inner = FakeRustTimeline() + val inner = FakeFfiTimeline() val systemClock = FakeSystemClock() val sut = createRustTimeline( inner = inner, @@ -51,7 +51,7 @@ class RustTimelineTest { runCurrent() inner.emitDiff( listOf( - FakeRustTimelineDiff( + FakeFfiTimelineDiff( item = null, change = TimelineChange.RESET, ) @@ -106,7 +106,7 @@ private fun TestScope.createRustTimeline( joinedRoom: JoinedRoom = FakeJoinedRoom().apply { givenRoomInfo(aRoomInfo()) }, coroutineScope: CoroutineScope = backgroundScope, dispatcher: CoroutineDispatcher = testCoroutineDispatchers().io, - roomContentForwarder: RoomContentForwarder = RoomContentForwarder(FakeRustRoomListService()), + roomContentForwarder: RoomContentForwarder = RoomContentForwarder(FakeFfiRoomListService()), featureFlagsService: FeatureFlagService = FakeFeatureFlagService(), onNewSyncedEvent: () -> Unit = {}, ): RustTimeline { diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt index ca41fe40d7..0aacf6ae9b 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt @@ -11,9 +11,9 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.impl.fixtures.factories.aRustEventTimelineItem -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimeline -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineDiff -import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineItem +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimeline +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineDiff +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiTimelineItem import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder import kotlinx.coroutines.CompletableDeferred @@ -35,7 +35,7 @@ class TimelineItemsSubscriberTest { fun `when timeline emits an empty list of items, the flow must emits an empty list`() = runTest { val timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) - val timeline = FakeRustTimeline() + val timeline = FakeFfiTimeline() val timelineItemsSubscriber = createTimelineItemsSubscriber( timeline = timeline, timelineItems = timelineItems, @@ -44,7 +44,7 @@ class TimelineItemsSubscriberTest { timelineItemsSubscriber.subscribeIfNeeded() // Wait for the listener to be set. runCurrent() - timeline.emitDiff(listOf(FakeRustTimelineDiff(item = null, change = TimelineChange.RESET))) + timeline.emitDiff(listOf(FakeFfiTimelineDiff(item = null, change = TimelineChange.RESET))) val final = awaitItem() assertThat(final).isEmpty() timelineItemsSubscriber.unsubscribeIfNeeded() @@ -55,7 +55,7 @@ class TimelineItemsSubscriberTest { fun `when timeline emits a non empty list of items, the flow must emits a non empty list`() = runTest { val timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) - val timeline = FakeRustTimeline() + val timeline = FakeFfiTimeline() val timelineItemsSubscriber = createTimelineItemsSubscriber( timeline = timeline, timelineItems = timelineItems, @@ -64,7 +64,7 @@ class TimelineItemsSubscriberTest { timelineItemsSubscriber.subscribeIfNeeded() // Wait for the listener to be set. runCurrent() - timeline.emitDiff(listOf(FakeRustTimelineDiff(item = FakeRustTimelineItem(), change = TimelineChange.RESET))) + timeline.emitDiff(listOf(FakeFfiTimelineDiff(item = FakeFfiTimelineItem(), change = TimelineChange.RESET))) val final = awaitItem() assertThat(final).isNotEmpty() timelineItemsSubscriber.unsubscribeIfNeeded() @@ -75,7 +75,7 @@ class TimelineItemsSubscriberTest { fun `when timeline emits an item with SYNC origin, the callback onNewSyncedEvent is invoked`() = runTest { val timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) - val timeline = FakeRustTimeline() + val timeline = FakeFfiTimeline() val onNewSyncedEventRecorder = lambdaRecorder { } val timelineItemsSubscriber = createTimelineItemsSubscriber( timeline = timeline, @@ -88,8 +88,8 @@ class TimelineItemsSubscriberTest { runCurrent() timeline.emitDiff( listOf( - FakeRustTimelineDiff( - item = FakeRustTimelineItem( + FakeFfiTimelineDiff( + item = FakeFfiTimelineItem( asEventResult = aRustEventTimelineItem(origin = EventItemOrigin.SYNC), ), change = TimelineChange.RESET, @@ -114,7 +114,7 @@ class TimelineItemsSubscriberTest { } private fun TestScope.createTimelineItemsSubscriber( - timeline: Timeline = FakeRustTimeline(), + timeline: Timeline = FakeFfiTimeline(), timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE), initLatch: CompletableDeferred = CompletableDeferred(), isTimelineInitialized: MutableStateFlow = MutableStateFlow(false), diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index bc4fc085df..7d3ee5e994 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -25,11 +25,11 @@ import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.BaseRoom import io.element.android.libraries.matrix.api.room.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.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.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser @@ -103,7 +103,7 @@ class FakeMatrixClient( private var createRoomResult: Result = Result.success(A_ROOM_ID) private var createDmResult: Result = Result.success(A_ROOM_ID) - private var findDmResult: RoomId? = A_ROOM_ID + private var findDmResult: Result = Result.success(A_ROOM_ID) private val getRoomResults = mutableMapOf() private val searchUserResults = mutableMapOf>() private val getProfileResults = mutableMapOf>() @@ -111,17 +111,17 @@ class FakeMatrixClient( private var setDisplayNameResult: Result = Result.success(Unit) private var uploadAvatarResult: Result = Result.success(Unit) private var removeAvatarResult: Result = Result.success(Unit) - var joinRoomLambda: (RoomId) -> Result = { + var joinRoomLambda: (RoomId) -> Result = { Result.success(null) } - var joinRoomByIdOrAliasLambda: (RoomIdOrAlias, List) -> Result = { _, _ -> + var joinRoomByIdOrAliasLambda: (RoomIdOrAlias, List) -> Result = { _, _ -> Result.success(null) } - var knockRoomLambda: (RoomIdOrAlias, String, List) -> Result = { _, _, _ -> + var knockRoomLambda: (RoomIdOrAlias, String, List) -> Result = { _, _, _ -> Result.success(null) } - var getRoomSummaryFlowLambda = { _: RoomIdOrAlias -> - flowOf>(Optional.empty()) + var getRoomInfoFlowLambda = { _: RoomId -> + flowOf>(Optional.empty()) } var logoutLambda: (Boolean, Boolean) -> Unit = { _, _ -> } @@ -133,7 +133,7 @@ class FakeMatrixClient( return getRoomResults[roomId] as? JoinedRoom } - override suspend fun findDM(userId: UserId): RoomId? { + override suspend fun findDM(userId: UserId): Result { return findDmResult } @@ -216,13 +216,13 @@ class FakeMatrixClient( return removeAvatarResult } - override suspend fun joinRoom(roomId: RoomId): Result = joinRoomLambda(roomId) + override suspend fun joinRoom(roomId: RoomId): Result = joinRoomLambda(roomId) - override suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result { + override suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List): Result { return joinRoomByIdOrAliasLambda(roomIdOrAlias, serverNames) } - override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result { + override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List): Result { return knockRoomLambda(roomIdOrAlias, message, serverNames) } @@ -248,7 +248,7 @@ class FakeMatrixClient( createDmResult = result } - fun givenFindDmResult(result: RoomId?) { + fun givenFindDmResult(result: Result) { findDmResult = result } @@ -304,7 +304,7 @@ class FakeMatrixClient( return Result.success(visitedRoomsId) } - override fun getRoomSummaryFlow(roomIdOrAlias: RoomIdOrAlias) = getRoomSummaryFlowLambda(roomIdOrAlias) + override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId) var setAllSendQueuesEnabledLambda = lambdaRecorder(ensureNeverCalled = true) { _: Boolean -> // no-op diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index 3a354df6b9..87b8a348dc 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -81,6 +81,7 @@ const val AN_AVATAR_URL = "mxc://data" const val A_FAILURE_REASON = "There has been a failure" +@Suppress("unused") val A_THROWABLE = Throwable(A_FAILURE_REASON) val AN_EXCEPTION = Exception(A_FAILURE_REASON) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt index d755722bd3..ae9aee8736 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeBaseRoom.kt @@ -54,6 +54,7 @@ class FakeBaseRoom( private val canUserJoinCallResult: (UserId) -> Result = { lambdaError() }, private val canUserPinUnpinResult: (UserId) -> Result = { lambdaError() }, private val setIsFavoriteResult: (Boolean) -> Result = { lambdaError() }, + private val markAsReadResult: (ReceiptType) -> Result = { Result.success(Unit) }, private val powerLevelsResult: () -> Result = { lambdaError() }, private val leaveRoomLambda: () -> Result = { lambdaError() }, private val updateMembersResult: () -> Unit = { lambdaError() }, @@ -183,11 +184,8 @@ class FakeBaseRoom( return setIsFavoriteResult(isFavorite) } - val markAsReadCalls = mutableListOf() - override suspend fun markAsRead(receiptType: ReceiptType): Result { - markAsReadCalls.add(receiptType) - return Result.success(Unit) + return markAsReadResult(receiptType) } var setUnreadFlagCalls = mutableListOf() diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt index eb46482c5c..eecacda543 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/EditableAvatarView.kt @@ -38,6 +38,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.Icon +import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings @@ -107,7 +108,9 @@ fun EditableAvatarView( @Composable internal fun EditableAvatarViewPreview( @PreviewParameter(EditableAvatarViewUriProvider::class) uri: Uri? -) = ElementPreview { +) = ElementPreview( + drawableFallbackForImages = CommonDrawables.sample_avatar, +) { EditableAvatarView( matrixId = "id", displayName = "A room", diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt index f3cecb35c5..3a31bf916e 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/UnsavedAvatar.kt @@ -29,7 +29,6 @@ import coil3.request.ImageRequest import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial @@ -55,7 +54,7 @@ fun UnsavedAvatar( AsyncImage( modifier = commonModifier, model = model, - placeholder = debugPlaceholderBackground(ColorPainter(MaterialTheme.colorScheme.surfaceVariant)), + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), contentScale = ContentScale.Crop, contentDescription = null, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChange.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChange.kt index 303fae4d48..0001131e59 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChange.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChange.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.matrix.ui.room -import androidx.compose.runtime.ProduceStateScope import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.UserId @@ -17,25 +16,25 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.ui.model.getAvatarData import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.flow @OptIn(ExperimentalCoroutinesApi::class) -fun JoinedRoom.roomMemberIdentityStateChange(): Flow> { - return roomInfoFlow - .filter { - // Room cannot become unencrypted, so we can just apply a filter here. - it.isEncrypted == true +fun JoinedRoom.roomMemberIdentityStateChange(waitForEncryption: Boolean): Flow> { + val encryptionChangeFlow = flow { + if (waitForEncryption) { + // Room cannot become unencrypted, so it's ok to use first here + roomInfoFlow.first { roomInfo -> roomInfo.isEncrypted == true } } - .distinctUntilChanged() + emit(Unit) + } + return encryptionChangeFlow .flatMapLatest { combine(identityStateChangesFlow, membersStateFlow) { identityStateChanges, membersState -> identityStateChanges.map { identityStateChange -> @@ -52,14 +51,6 @@ fun JoinedRoom.roomMemberIdentityStateChange(): Flow>.observeRoomMemberIdentityStateChange(room: JoinedRoom) { - room.roomMemberIdentityStateChange() - .onEach { roomMemberIdentityStateChanges -> - value = roomMemberIdentityStateChanges.toPersistentList() - } - .launchIn(this) -} - private fun RoomMember.toIdentityRoomMember() = IdentityRoomMember( userId = userId, displayNameOrDefault = displayNameOrDefault, diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChangeTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChangeTest.kt new file mode 100644 index 0000000000..8abf5092e6 --- /dev/null +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/room/ObserveRoomMemberIdentityStateChangeTest.kt @@ -0,0 +1,403 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.ui.room + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.encryption.identity.IdentityState +import io.element.android.libraries.matrix.api.encryption.identity.IdentityStateChange +import io.element.android.libraries.matrix.api.room.RoomMembersState +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_ID_3 +import io.element.android.libraries.matrix.test.room.FakeJoinedRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.room.aRoomMember +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ObserveRoomMemberIdentityStateChangeTest { + private val aliceRoomMember = aRoomMember(A_USER_ID, displayName = "Alice") + private val bobRoomMember = aRoomMember(A_USER_ID_2, displayName = "Bob") + private val carolRoomMember = aRoomMember(A_USER_ID_3, displayName = "Carol") + + @Test + fun `roomMemberIdentityStateChange emits empty list for non-encrypted room with no identity changes`() = + runTest { + val identityStateChangesFlow = MutableStateFlow>(emptyList()) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = false)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test { + val result = awaitItem() + assertThat(result).isEmpty() + } + } + + @Test + fun `roomMemberIdentityStateChange emits identity changes for non-encrypted room when waitForEncryption is false`() = + runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf( + IdentityStateChange(bobRoomMember.userId, IdentityState.Verified), + IdentityStateChange(carolRoomMember.userId, IdentityState.PinViolation) + ) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = false)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember, + carolRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test { + val result = awaitItem() + assertThat(result).hasSize(2) + + val bobChange = result.find { it.identityRoomMember.userId == bobRoomMember.userId } + assertThat(bobChange).isNotNull() + assertThat(bobChange?.identityState).isEqualTo(IdentityState.Verified) + assertThat(bobChange?.identityRoomMember?.displayNameOrDefault).isEqualTo("Bob") + + val carolChange = result.find { it.identityRoomMember.userId == carolRoomMember.userId } + assertThat(carolChange).isNotNull() + assertThat(carolChange?.identityState).isEqualTo(IdentityState.PinViolation) + assertThat(carolChange?.identityRoomMember?.displayNameOrDefault).isEqualTo("Carol") + } + } + + @Test + fun `roomMemberIdentityStateChange emits identity changes for already encrypted room`() = + runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf( + IdentityStateChange(bobRoomMember.userId, IdentityState.VerificationViolation) + ) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = true)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = true).test { + val result = awaitItem() + assertThat(result).hasSize(1) + + val bobChange = result.first() + assertThat(bobChange.identityRoomMember.userId).isEqualTo(bobRoomMember.userId) + assertThat(bobChange.identityState).isEqualTo(IdentityState.VerificationViolation) + assertThat(bobChange.identityRoomMember.displayNameOrDefault).isEqualTo("Bob") + } + } + + @Test + fun `roomMemberIdentityStateChange waits for encryption before emitting when waitForEncryption is true`() = + runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf(IdentityStateChange(bobRoomMember.userId, IdentityState.Pinned)) + ) + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = false)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = true).test { + // Should not emit anything yet since room is not encrypted + expectNoEvents() + + // Enable encryption + joinedRoom.baseRoom.givenRoomInfo(aRoomInfo(isEncrypted = true)) + + val result = awaitItem() + assertThat(result).hasSize(1) + assertThat(result.first().identityRoomMember.userId).isEqualTo(bobRoomMember.userId) + assertThat(result.first().identityState).isEqualTo(IdentityState.Pinned) + } + } + + @Test + fun `roomMemberIdentityStateChange creates default member when room member not found`() = + runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf(IdentityStateChange(carolRoomMember.userId, IdentityState.PinViolation)) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = true)) + // Only include aliceRoomMember and bobRoomMember, not carolRoomMember + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test { + val result = awaitItem() + assertThat(result).hasSize(1) + + val carolChange = result.first() + assertThat(carolChange.identityRoomMember.userId).isEqualTo(carolRoomMember.userId) + assertThat(carolChange.identityState).isEqualTo(IdentityState.PinViolation) + // Should use extracted display name from user ID since member not found + assertThat(carolChange.identityRoomMember.displayNameOrDefault).isEqualTo( + carolRoomMember.userId.extractedDisplayName + ) + } + } + + @Test + fun `roomMemberIdentityStateChange updates when identity state changes`() = runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf(IdentityStateChange(bobRoomMember.userId, IdentityState.Pinned)) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = true)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test { + val firstResult = awaitItem() + assertThat(firstResult).hasSize(1) + assertThat(firstResult.first().identityState).isEqualTo(IdentityState.Pinned) + + // Update identity state + identityStateChangesFlow.value = listOf( + IdentityStateChange(bobRoomMember.userId, IdentityState.VerificationViolation) + ) + + val secondResult = awaitItem() + assertThat(secondResult).hasSize(1) + assertThat(secondResult.first().identityState).isEqualTo(IdentityState.VerificationViolation) + } + } + + @Test + fun `roomMemberIdentityStateChange updates when members state changes`() = runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf(IdentityStateChange(bobRoomMember.userId, IdentityState.Verified)) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = true)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test { + val firstResult = awaitItem() + assertThat(firstResult).hasSize(1) + assertThat(firstResult.first().identityRoomMember.displayNameOrDefault).isEqualTo("Bob") + + // Update room member with different display name + val updatedMember2 = bobRoomMember.copy(displayName = "Bobby") + joinedRoom.baseRoom.givenRoomMembersState( + RoomMembersState.Ready(persistentListOf(aliceRoomMember, updatedMember2)) + ) + + val secondResult = awaitItem() + assertThat(secondResult).hasSize(1) + assertThat(secondResult.first().identityRoomMember.displayNameOrDefault).isEqualTo("Bobby") + } + } + + @Test + fun `roomMemberIdentityStateChange handles multiple identity states`() = runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf( + IdentityStateChange(aliceRoomMember.userId, IdentityState.Verified), + IdentityStateChange(bobRoomMember.userId, IdentityState.PinViolation), + IdentityStateChange(carolRoomMember.userId, IdentityState.VerificationViolation) + ) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = true)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember, + carolRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test { + val result = awaitItem() + assertThat(result).hasSize(3) + + val verifiedUser = result.find { it.identityState == IdentityState.Verified } + assertThat(verifiedUser?.identityRoomMember?.userId).isEqualTo(aliceRoomMember.userId) + + val pinViolationUser = result.find { it.identityState == IdentityState.PinViolation } + assertThat(pinViolationUser?.identityRoomMember?.userId).isEqualTo(bobRoomMember.userId) + + val verificationViolationUser = + result.find { it.identityState == IdentityState.VerificationViolation } + assertThat(verificationViolationUser?.identityRoomMember?.userId).isEqualTo(carolRoomMember.userId) + } + } + + @Test + fun `roomMemberIdentityStateChange handles room becoming encrypted scenario`() = runTest { + val identityStateChangesFlow = MutableStateFlow( + listOf(IdentityStateChange(bobRoomMember.userId, IdentityState.Pinned)) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = false)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = true).test { + // Should not emit anything initially as room is not encrypted + expectNoEvents() + + // Room becomes encrypted + joinedRoom.baseRoom.givenRoomInfo(aRoomInfo(isEncrypted = true)) + + val result = awaitItem() + assertThat(result).hasSize(1) + assertThat(result.first().identityRoomMember.userId).isEqualTo(bobRoomMember.userId) + assertThat(result.first().identityState).isEqualTo(IdentityState.Pinned) + + // Add more identity changes after encryption is enabled + identityStateChangesFlow.value = listOf( + IdentityStateChange(bobRoomMember.userId, IdentityState.Pinned), + IdentityStateChange(aliceRoomMember.userId, IdentityState.VerificationViolation) + ) + + val updatedResult = awaitItem() + assertThat(updatedResult).hasSize(2) + } + } + + @Test + fun `roomMemberIdentityStateChange does not emit duplicates for same state`() = runTest { + val identityStateChangesFlow = MutableSharedFlow>() + val identityStateChanges = listOf( + IdentityStateChange(bobRoomMember.userId, IdentityState.Verified) + ) + + val joinedRoom = FakeJoinedRoom( + identityStateChangesFlow = identityStateChangesFlow, + baseRoom = FakeJoinedRoom().baseRoom.apply { + givenRoomInfo(aRoomInfo(isEncrypted = true)) + givenRoomMembersState( + RoomMembersState.Ready( + persistentListOf( + aliceRoomMember, + bobRoomMember + ) + ) + ) + } + ) + + joinedRoom.roomMemberIdentityStateChange(waitForEncryption = false).test { + identityStateChangesFlow.emit(identityStateChanges) + + val firstResult = awaitItem() + assertThat(firstResult).hasSize(1) + + // Emit the same state again + identityStateChangesFlow.emit(identityStateChanges) + + // Should not emit a new item due to distinctUntilChanged + expectNoEvents() + } + } +} diff --git a/libraries/mediaplayer/impl/build.gradle.kts b/libraries/mediaplayer/impl/build.gradle.kts index 9d7bdfd1ae..6bd19680a3 100644 --- a/libraries/mediaplayer/impl/build.gradle.kts +++ b/libraries/mediaplayer/impl/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(libs.dagger) implementation(projects.libraries.audio.api) + implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(libs.coroutines.core) diff --git a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt index cda3bd94b3..45e600f8b9 100644 --- a/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt +++ b/libraries/mediaplayer/impl/src/main/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayer.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.audio.api.AudioFocus import io.element.android.libraries.audio.api.AudioFocusRequester import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.mediaplayer.api.MediaPlayer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview @@ -37,7 +38,8 @@ import kotlin.time.Duration.Companion.seconds @SingleIn(RoomScope::class) class DefaultMediaPlayer @Inject constructor( private val player: SimplePlayer, - private val coroutineScope: CoroutineScope, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, private val audioFocus: AudioFocus, ) : MediaPlayer { private val listener = object : SimplePlayer.Listener { @@ -50,7 +52,7 @@ class DefaultMediaPlayer @Inject constructor( ) } if (isPlaying) { - job = coroutineScope.launch { updateCurrentPosition() } + job = sessionCoroutineScope.launch { updateCurrentPosition() } } else { audioFocus.releaseAudioFocus() job?.cancel() diff --git a/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt index 8a36671fc2..a91a204378 100644 --- a/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt +++ b/libraries/mediaplayer/impl/src/test/kotlin/io/element/android/libraries/mediaplayer/impl/DefaultMediaPlayerTest.kt @@ -337,6 +337,7 @@ class DefaultMediaPlayerTest { duration = null, ) ) + @Suppress("RunCatchingNotAllowed") val result = runCatching { sut.setMedia("uri", "mediaId", "mimeType", 12) } @@ -422,7 +423,7 @@ class DefaultMediaPlayerTest { audioFocus: AudioFocus = FakeAudioFocus(), ): DefaultMediaPlayer = DefaultMediaPlayer( player = simplePlayer, - coroutineScope = backgroundScope, + sessionCoroutineScope = backgroundScope, audioFocus = audioFocus, ) } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index 47451faef4..5f542bd91a 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -192,8 +192,10 @@ class MediaSender @Inject constructor( } } + // We handle the cancellations here manually, so we suppress the warning + @Suppress("RunCatchingNotAllowed") return handler - .flatMapCatching { uploadHandler -> + .mapCatching { uploadHandler -> ongoingUploadJobs[Job] = uploadHandler uploadHandler.await() } diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt index f438d1e833..6363fa06c9 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.androidutils.media.runAndRelease import io.element.android.libraries.core.coroutine.CoroutineDispatchers 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.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage @@ -73,7 +74,7 @@ class AndroidMediaPreProcessor @Inject constructor( deleteOriginal: Boolean, compressIfPossible: Boolean, ): Result = withContext(coroutineDispatchers.computation) { - runCatching { + runCatchingExceptions { val result = when { // Special case for SVG, since Android can't read its metadata or create a thumbnail, it must be sent as a file mimeType == MimeTypes.Svg -> { @@ -188,7 +189,7 @@ class AndroidMediaPreProcessor @Inject constructor( } private suspend fun processVideo(uri: Uri, mimeType: String?, shouldBeCompressed: Boolean): MediaUploadInfo { - val resultFile = runCatching { + val resultFile = runCatchingExceptions { videoCompressor.compress(uri, shouldBeCompressed) .onEach { // TODO handle progress diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt index 2d66a68b06..f14264c5d7 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.androidutils.bitmap.resizeToMax import io.element.android.libraries.androidutils.bitmap.rotateToMetadataOrientation import io.element.android.libraries.androidutils.file.createTmpFile import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.ApplicationContext import kotlinx.coroutines.withContext import java.io.File @@ -38,7 +39,7 @@ class ImageCompressor @Inject constructor( orientation: Int = ExifInterface.ORIENTATION_UNDEFINED, desiredQuality: Int = 78, ): Result = withContext(dispatchers.io) { - runCatching { + runCatchingExceptions { val format = mimeTypeToCompressFormat(mimeType) val extension = mimeTypeToCompressFileExtension(mimeType) val compressedBitmap = compressToBitmap(inputStreamProvider, resizeMode, orientation).getOrThrow() @@ -65,7 +66,7 @@ class ImageCompressor @Inject constructor( inputStreamProvider: () -> InputStream, resizeMode: ResizeMode, orientation: Int, - ): Result = runCatching { + ): Result = runCatchingExceptions { val options = BitmapFactory.Options() // Decode bounds inputStreamProvider().use { input -> diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt index db3666a3b4..9f36b49bc0 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt @@ -22,6 +22,7 @@ import com.otaliastudios.transcoder.validator.WriteAlwaysValidator import io.element.android.libraries.androidutils.file.createTmpFile import io.element.android.libraries.androidutils.file.getMimeType import io.element.android.libraries.androidutils.file.safeDelete +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.ApplicationContext import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow @@ -81,7 +82,7 @@ class VideoCompressor @Inject constructor( } private fun getVideoMetadata(uri: Uri): VideoFileMetadata? { - return runCatching { + return runCatchingExceptions { MediaMetadataRetriever().use { it.setDataSource(context, uri) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt index f4764e2d6b..27fcf2ce3e 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenter.kt @@ -22,6 +22,7 @@ import dagger.assisted.AssistedInject import io.element.android.libraries.androidutils.R import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState @@ -154,7 +155,7 @@ class MediaGalleryPresenter @AssistedInject constructor( mimeType = mediaItem.mediaInfo().mimeType, filename = mediaItem.mediaInfo().filename ) - .mapCatching { mediaFile -> + .mapCatchingExceptions { mediaFile -> localMediaFactory.createFromMediaFile( mediaFile = mediaFile, mediaInfo = mediaItem.mediaInfo() @@ -164,7 +165,7 @@ class MediaGalleryPresenter @AssistedInject constructor( private suspend fun saveOnDisk(mediaItem: MediaItem.Event) { downloadMedia(mediaItem) - .mapCatching { localMedia -> + .mapCatchingExceptions { localMedia -> localMediaActions.saveOnDisk(localMedia) } .onSuccess { @@ -179,7 +180,7 @@ class MediaGalleryPresenter @AssistedInject constructor( private suspend fun share(mediaItem: MediaItem.Event) { downloadMedia(mediaItem) - .mapCatching { localMedia -> + .mapCatchingExceptions { localMedia -> localMediaActions.share(localMedia) } .onFailure { diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt index 9ec3e68ae1..76841a9153 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/ImageItemView.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box @@ -22,16 +21,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalInspectionMode import coil3.compose.AsyncImage import coil3.compose.AsyncImagePainter -import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.mediaviewer.impl.model.MediaItem import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage -@OptIn(ExperimentalFoundationApi::class) @Composable fun ImageItemView( image: MediaItem.Image, @@ -39,16 +35,10 @@ fun ImageItemView( onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { - val bgColor = if (LocalInspectionMode.current) { - ElementTheme.colors.bgDecorative1 - } else { - Color.Transparent - } Box( modifier = modifier .aspectRatio(1f) - .combinedClickable(onClick = onClick, onLongClick = onLongClick) - .background(bgColor), + .combinedClickable(onClick = onClick, onLongClick = onLongClick), ) { var isLoaded by remember { mutableStateOf(false) } AsyncImage( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt index cf4db1f503..0f49028d34 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/ui/VideoItemView.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.mediaviewer.impl.gallery.ui -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box @@ -27,7 +26,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage @@ -40,7 +38,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.mediaviewer.impl.model.MediaItem -@OptIn(ExperimentalFoundationApi::class) @Composable fun VideoItemView( video: MediaItem.Video, @@ -48,16 +45,10 @@ fun VideoItemView( onLongClick: () -> Unit, modifier: Modifier = Modifier, ) { - val bgColor = if (LocalInspectionMode.current) { - ElementTheme.colors.bgDecorative2 - } else { - Color.Transparent - } Box( modifier = modifier .aspectRatio(1f) - .combinedClickable(onClick = onClick, onLongClick = onLongClick) - .background(bgColor), + .combinedClickable(onClick = onClick, onLongClick = onLongClick), ) { var isLoaded by remember { mutableStateOf(false) } AsyncImage( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt index 56af3ca2c8..d6c43ce4c4 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt @@ -32,6 +32,7 @@ import androidx.core.net.toFile import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.androidutils.system.startInstallFromSourceIntent import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.AppScope @@ -83,7 +84,7 @@ class AndroidLocalMediaActions @Inject constructor( override suspend fun saveOnDisk(localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE) - runCatching { + runCatchingExceptions { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { saveOnDiskUsingMediaStore(localMedia) } else { @@ -98,7 +99,7 @@ class AndroidLocalMediaActions @Inject constructor( override suspend fun share(localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE) - runCatching { + runCatchingExceptions { val shareableUri = localMedia.toShareableUri() val shareMediaIntent = Intent(Intent.ACTION_SEND) .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) @@ -117,7 +118,7 @@ class AndroidLocalMediaActions @Inject constructor( override suspend fun open(localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE) - runCatching { + runCatchingExceptions { when (localMedia.info.mimeType) { MimeTypes.Apk -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/ParcelFileDescriptorFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/ParcelFileDescriptorFactory.kt index fc1110086a..9f6d10b6b0 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/ParcelFileDescriptorFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/ParcelFileDescriptorFactory.kt @@ -10,10 +10,11 @@ package io.element.android.libraries.mediaviewer.impl.local.pdf import android.content.Context import android.net.Uri import android.os.ParcelFileDescriptor +import io.element.android.libraries.core.extensions.runCatchingExceptions import java.io.File class ParcelFileDescriptorFactory(private val context: Context) { - fun create(model: Any?) = runCatching { + fun create(model: Any?) = runCatchingExceptions { when (model) { is File -> ParcelFileDescriptor.open(model, ParcelFileDescriptor.MODE_READ_ONLY) is Uri -> context.contentResolver.openFileDescriptor(model, "r")!! diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfRendererManager.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfRendererManager.kt index fa3719bf8b..c7562e98e7 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfRendererManager.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/pdf/PdfRendererManager.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.local.pdf import android.graphics.pdf.PdfRenderer import android.os.ParcelFileDescriptor import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.extensions.runCatchingExceptions import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope @@ -35,7 +36,7 @@ class PdfRendererManager( coroutineScope.launch { mutex.withLock { withContext(Dispatchers.IO) { - pdfRenderer = runCatching { + pdfRenderer = runCatchingExceptions { PdfRenderer(parcelFileDescriptor) }.fold( onSuccess = { pdfRenderer -> diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileView.kt index c025ab8bed..4c4429b775 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/txt/TextFileView.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.viewfolder.api.TextFileViewer import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator @@ -43,7 +44,7 @@ fun TextFileView( data.value = AsyncData.Loading() if (localMedia?.uri != null) { // Load the file content - val result = runCatching { + val result = runCatchingExceptions { context.contentResolver.openInputStream(localMedia.uri).use { it?.bufferedReader()?.readLines()?.toList().orEmpty() } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt index 4bb37abf65..b2eb7e7b6f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.timeline.Timeline @@ -173,7 +174,7 @@ class MediaViewerDataSource( .onSuccess { mediaFile -> mediaFiles.add(mediaFile) } - .mapCatching { mediaFile -> + .mapCatchingExceptions { mediaFile -> localMediaFactory.createFromMediaFile( mediaFile = mediaFile, mediaInfo = data.mediaInfo diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerFlickToDismiss.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerFlickToDismiss.kt index 1b99cdfa03..43880809d9 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerFlickToDismiss.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerFlickToDismiss.kt @@ -18,11 +18,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import io.element.android.compound.theme.ElementTheme import kotlinx.coroutines.delay +import me.saket.telephoto.ExperimentalTelephotoApi import me.saket.telephoto.flick.FlickToDismiss import me.saket.telephoto.flick.FlickToDismissState import me.saket.telephoto.flick.rememberFlickToDismissState import kotlin.time.Duration +@OptIn(ExperimentalTelephotoApi::class) @Composable fun MediaViewerFlickToDismiss( onDismiss: () -> Unit, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt index d365362c78..a4a7f2209f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt @@ -37,6 +37,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged @@ -554,8 +555,11 @@ private fun ThumbnailView( source = thumbnailSource, kind = MediaRequestData.Kind.File(mediaInfo.filename, mediaInfo.mimeType) ) + val alpha = if (LocalInspectionMode.current) 0.1f else 1f AsyncImage( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .alpha(alpha), model = mediaRequestData, contentScale = ContentScale.Fit, contentDescription = null, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt index c71cdb8573..1596104c46 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDeleteConfirmationBottomSheetTest.kt @@ -19,6 +19,7 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam 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 org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -60,7 +61,7 @@ private fun AndroidComposeTestRule.setMedia onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDismiss: () -> Unit = EnsureNeverCalled(), ) { - setContent { + setSafeContent { MediaDeleteConfirmationBottomSheet( state = state, onDelete = onDelete, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt index b74ae07b8b..1289d73672 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheetTest.kt @@ -18,6 +18,7 @@ import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.clickOn import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.setSafeContent import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule @@ -100,7 +101,7 @@ private fun AndroidComposeTestRule.setMedia onDelete: (EventId) -> Unit = EnsureNeverCalledWithParam(), onDismiss: () -> Unit = EnsureNeverCalled(), ) { - setContent { + setSafeContent { MediaDetailsBottomSheet( state = state, onViewInTimeline = onViewInTimeline, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt index ba9881a786..74d83b733b 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerViewTest.kt @@ -26,6 +26,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 io.mockk.mockk import org.junit.Rule import org.junit.Test @@ -249,7 +250,7 @@ private fun AndroidComposeTestRule.setMedia state: MediaViewerState, onBackClick: () -> Unit = EnsureNeverCalled(), ) { - setContent { + setSafeContent { MediaViewerView( state = state, audioFocus = null, diff --git a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt b/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt deleted file mode 100644 index d216afc34f..0000000000 --- a/libraries/oidc/api/src/main/kotlin/io/element/android/libraries/oidc/api/OidcEntryPoint.kt +++ /dev/null @@ -1,18 +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.oidc.api - -import android.app.Activity -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node - -interface OidcEntryPoint { - fun canUseCustomTab(): Boolean - fun openUrlInCustomTab(activity: Activity, darkTheme: Boolean, url: String) - fun createFallbackWebViewNode(parentNode: Node, buildContext: BuildContext, url: String): Node -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt deleted file mode 100644 index 077331dbdf..0000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/CustomTabAvailabilityChecker.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.oidc.impl - -import android.content.Context -import androidx.browser.customtabs.CustomTabsClient -import io.element.android.libraries.di.ApplicationContext -import javax.inject.Inject - -class CustomTabAvailabilityChecker @Inject constructor( - @ApplicationContext private val context: Context, -) { - /** - * Return true if the device supports Custom tab, i.e. there is an third party app with - * CustomTab support (ex: Chrome, Firefox, etc.). - */ - fun supportCustomTab(): Boolean { - val packageName = CustomTabsClient.getPackageName(context, null) - return packageName != null - } -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt similarity index 95% rename from libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt rename to libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt index 39b6795725..e975cde261 100644 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlow.kt +++ b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlow.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oidc.impl.customtab +package io.element.android.libraries.oidc.impl import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt deleted file mode 100644 index 00f8838d42..0000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcEntryPoint.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.oidc.impl - -import android.app.Activity -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab -import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.matrix.api.auth.OidcDetails -import io.element.android.libraries.oidc.api.OidcEntryPoint -import io.element.android.libraries.oidc.impl.webview.OidcNode -import javax.inject.Inject - -@ContributesBinding(AppScope::class) -class DefaultOidcEntryPoint @Inject constructor( - private val customTabAvailabilityChecker: CustomTabAvailabilityChecker, -) : OidcEntryPoint { - override fun canUseCustomTab(): Boolean { - return customTabAvailabilityChecker.supportCustomTab() - } - - override fun openUrlInCustomTab(activity: Activity, darkTheme: Boolean, url: String) { - assert(canUseCustomTab()) { "Custom tab is not supported in this device." } - activity.openUrlInChromeCustomTab(null, darkTheme, url) - } - - override fun createFallbackWebViewNode(parentNode: Node, buildContext: BuildContext, url: String): Node { - assert(!canUseCustomTab()) { "Custom tab should be used instead of the fallback node." } - val inputs = OidcNode.Inputs(OidcDetails(url)) - return parentNode.createNode(buildContext, listOf(inputs)) - } -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt deleted file mode 100644 index 64c21596c1..0000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/customtab/CustomTabHandler.kt +++ /dev/null @@ -1,69 +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.oidc.impl.customtab - -import android.app.Activity -import android.content.ComponentName -import android.content.Context -import androidx.browser.customtabs.CustomTabsClient -import androidx.browser.customtabs.CustomTabsServiceConnection -import androidx.browser.customtabs.CustomTabsSession -import androidx.core.net.toUri -import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab -import io.element.android.libraries.di.ApplicationContext -import javax.inject.Inject - -class CustomTabHandler @Inject constructor( - @ApplicationContext private val context: Context, -) { - private var customTabsSession: CustomTabsSession? = null - private var customTabsClient: CustomTabsClient? = null - private var customTabsServiceConnection: CustomTabsServiceConnection? = null - - fun prepareCustomTab(url: String) { - val packageName = CustomTabsClient.getPackageName(context, null) - - // packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device - if (packageName != null) { - customTabsServiceConnection = object : CustomTabsServiceConnection() { - override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) { - customTabsClient = client.apply { warmup(0L) } - prefetchUrl(url) - } - - override fun onServiceDisconnected(name: ComponentName?) { - } - } - .also { - CustomTabsClient.bindCustomTabsService( - context, - // Despite the API, packageName cannot be null - packageName, - it - ) - } - } - } - - private fun prefetchUrl(url: String) { - if (customTabsSession == null) { - customTabsSession = customTabsClient?.newSession(null) - } - - customTabsSession?.mayLaunchUrl(url.toUri(), null, null) - } - - fun disposeCustomTab() { - customTabsServiceConnection?.let { context.unbindService(it) } - customTabsServiceConnection = null - } - - fun open(activity: Activity, darkTheme: Boolean, url: String) { - activity.openUrlInChromeCustomTab(customTabsSession, darkTheme, url) - } -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt deleted file mode 100644 index 165ef00ebc..0000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcEvents.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2023, 2024 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.libraries.oidc.impl.webview - -import io.element.android.libraries.oidc.api.OidcAction - -sealed interface OidcEvents { - data object Cancel : OidcEvents - data class OidcActionEvent(val oidcAction: OidcAction) : OidcEvents - data object ClearError : OidcEvents -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt deleted file mode 100644 index a9141dc333..0000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcNode.kt +++ /dev/null @@ -1,48 +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.oidc.impl.webview - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.architecture.NodeInputs -import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.matrix.api.auth.OidcDetails -import io.element.android.libraries.oidc.impl.OidcUrlParser - -@ContributesNode(AppScope::class) -class OidcNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - presenterFactory: OidcPresenter.Factory, - private val oidcUrlParser: OidcUrlParser, -) : Node(buildContext, plugins = plugins) { - data class Inputs( - val oidcDetails: OidcDetails, - ) : NodeInputs - - private val inputs: Inputs = inputs() - private val presenter = presenterFactory.create(inputs.oidcDetails) - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - OidcView( - state = state, - oidcUrlParser = oidcUrlParser, - modifier = modifier, - onNavigateBack = ::navigateUp, - ) - } -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt deleted file mode 100644 index f8144d69a6..0000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenter.kt +++ /dev/null @@ -1,90 +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.oidc.impl.webview - -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 dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.auth.OidcDetails -import io.element.android.libraries.oidc.api.OidcAction -import kotlinx.coroutines.launch - -class OidcPresenter @AssistedInject constructor( - @Assisted private val oidcDetails: OidcDetails, - private val authenticationService: MatrixAuthenticationService, -) : Presenter { - @AssistedFactory - interface Factory { - fun create(oidcDetails: OidcDetails): OidcPresenter - } - - @Composable - override fun present(): OidcState { - var requestState: AsyncAction by remember { - mutableStateOf(AsyncAction.Uninitialized) - } - val localCoroutineScope = rememberCoroutineScope() - - fun handleCancel() { - requestState = AsyncAction.Loading - localCoroutineScope.launch { - authenticationService.cancelOidcLogin() - .fold( - onSuccess = { - // Then go back - requestState = AsyncAction.Success(Unit) - }, - onFailure = { - requestState = AsyncAction.Failure(it) - } - ) - } - } - - fun handleSuccess(url: String) { - requestState = AsyncAction.Loading - localCoroutineScope.launch { - authenticationService.loginWithOidc(url) - .onFailure { - requestState = AsyncAction.Failure(it) - } - // On success, the node tree will be updated, there is nothing to do - } - } - - fun handleAction(action: OidcAction) { - when (action) { - OidcAction.GoBack -> handleCancel() - is OidcAction.Success -> handleSuccess(action.url) - } - } - - fun handleEvents(event: OidcEvents) { - when (event) { - OidcEvents.Cancel -> handleCancel() - is OidcEvents.OidcActionEvent -> handleAction(event.oidcAction) - OidcEvents.ClearError -> requestState = AsyncAction.Uninitialized - } - } - - return OidcState( - oidcDetails = oidcDetails, - requestState = requestState, - eventSink = ::handleEvents - ) - } -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt deleted file mode 100644 index 4358d70179..0000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcState.kt +++ /dev/null @@ -1,17 +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.oidc.impl.webview - -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.auth.OidcDetails - -data class OidcState( - val oidcDetails: OidcDetails, - val requestState: AsyncAction, - val eventSink: (OidcEvents) -> Unit -) diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt deleted file mode 100644 index 52d90a86a5..0000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcStateProvider.kt +++ /dev/null @@ -1,30 +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.oidc.impl.webview - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.api.auth.OidcDetails - -open class OidcStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aOidcState(), - aOidcState().copy(requestState = AsyncAction.Loading), - ) -} - -fun aOidcState() = OidcState( - oidcDetails = aOidcDetails(), - requestState = AsyncAction.Uninitialized, - eventSink = {} -) - -fun aOidcDetails() = OidcDetails( - url = "aUrl", -) diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt deleted file mode 100644 index 13b80dd600..0000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcView.kt +++ /dev/null @@ -1,117 +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.oidc.impl.webview - -import android.annotation.SuppressLint -import android.webkit.WebView -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.viewinterop.AndroidView -import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.designsystem.components.async.AsyncActionView -import io.element.android.libraries.designsystem.components.button.BackButton -import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.Scaffold -import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.oidc.impl.OidcUrlParser - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun OidcView( - state: OidcState, - oidcUrlParser: OidcUrlParser, - onNavigateBack: () -> Unit, - modifier: Modifier = Modifier, -) { - val isPreview = LocalInspectionMode.current - var webView by remember { mutableStateOf(null) } - fun shouldOverrideUrl(url: String): Boolean { - val action = oidcUrlParser.parse(url) - if (action != null) { - state.eventSink.invoke(OidcEvents.OidcActionEvent(action)) - return true - } - return false - } - - val oidcWebViewClient = remember { - OidcWebViewClient(::shouldOverrideUrl) - } - - fun onBack() { - if (webView?.canGoBack().orFalse()) { - webView?.goBack() - } else { - // To properly cancel Oidc login - state.eventSink.invoke(OidcEvents.Cancel) - } - } - - BackHandler { onBack() } - - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = {}, - navigationIcon = { - BackButton(onClick = ::onBack) - }, - ) - } - ) { contentPadding -> - AndroidView( - modifier = Modifier.padding(contentPadding), - factory = { context -> - WebView(context).apply { - if (!isPreview) { - webViewClient = oidcWebViewClient - settings.apply { - @SuppressLint("SetJavaScriptEnabled") - javaScriptEnabled = true - allowContentAccess = true - allowFileAccess = true - @Suppress("DEPRECATION") - databaseEnabled = true - domStorageEnabled = true - } - loadUrl(state.oidcDetails.url) - } - }.also { - webView = it - } - } - ) - - AsyncActionView( - async = state.requestState, - onSuccess = { onNavigateBack() }, - onErrorDismiss = { state.eventSink(OidcEvents.ClearError) } - ) - } -} - -@PreviewsDayNight -@Composable -internal fun OidcViewPreview(@PreviewParameter(OidcStateProvider::class) state: OidcState) = ElementPreview { - OidcView( - state = state, - oidcUrlParser = { null }, - onNavigateBack = {}, - ) -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt deleted file mode 100644 index 0d00c790fb..0000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/OidcWebViewClient.kt +++ /dev/null @@ -1,29 +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.oidc.impl.webview - -import android.webkit.WebResourceRequest -import android.webkit.WebView -import android.webkit.WebViewClient - -class OidcWebViewClient( - private val eventListener: WebViewEventListener, -) : WebViewClient() { - override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { - return shouldOverrideUrl(request.url.toString()) - } - - @Deprecated("Deprecated in Java") - override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { - return shouldOverrideUrl(url) - } - - private fun shouldOverrideUrl(url: String): Boolean { - return eventListener.shouldOverrideUrlLoading(url) - } -} diff --git a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt b/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt deleted file mode 100644 index def6e54c71..0000000000 --- a/libraries/oidc/impl/src/main/kotlin/io/element/android/libraries/oidc/impl/webview/WebViewEventListener.kt +++ /dev/null @@ -1,18 +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.oidc.impl.webview - -fun interface WebViewEventListener { - /** - * Triggered when a Webview loads an url. - * - * @param url The url about to be rendered. - * @return true if the method needs to manage some custom handling - */ - fun shouldOverrideUrlLoading(url: String): Boolean -} diff --git a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlowTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlowTest.kt similarity index 94% rename from libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlowTest.kt rename to libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlowTest.kt index 104ff6c347..3b56f28c5a 100644 --- a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/customtab/DefaultOidcActionFlowTest.kt +++ b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/DefaultOidcActionFlowTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.oidc.impl.customtab +package io.element.android.libraries.oidc.impl import com.google.common.truth.Truth.assertThat import io.element.android.libraries.oidc.api.OidcAction diff --git a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt b/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt deleted file mode 100644 index 433c77f73a..0000000000 --- a/libraries/oidc/impl/src/test/kotlin/io/element/android/libraries/oidc/impl/webview/OidcPresenterTest.kt +++ /dev/null @@ -1,142 +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(ExperimentalCoroutinesApi::class) - -package io.element.android.libraries.oidc.impl.webview - -import app.cash.molecule.RecompositionMode -import app.cash.molecule.moleculeFlow -import app.cash.turbine.test -import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.matrix.test.A_THROWABLE -import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA -import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService -import io.element.android.libraries.oidc.api.OidcAction -import io.element.android.tests.testutils.WarmUpRule -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test - -class OidcPresenterTest { - @get:Rule - val warmUpRule = WarmUpRule() - - @Test - fun `present - initial state`() = runTest { - val presenter = OidcPresenter( - A_OIDC_DATA, - FakeMatrixAuthenticationService(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.oidcDetails).isEqualTo(A_OIDC_DATA) - assertThat(initialState.requestState).isEqualTo(AsyncAction.Uninitialized) - } - } - - @Test - fun `present - go back`() = runTest { - val presenter = OidcPresenter( - A_OIDC_DATA, - FakeMatrixAuthenticationService(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(OidcEvents.Cancel) - val loadingState = awaitItem() - assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading) - val finalState = awaitItem() - assertThat(finalState.requestState).isEqualTo(AsyncAction.Success(Unit)) - } - } - - @Test - fun `present - go back with failure`() = runTest { - val authenticationService = FakeMatrixAuthenticationService() - val presenter = OidcPresenter( - A_OIDC_DATA, - authenticationService, - ) - authenticationService.givenOidcCancelError(A_THROWABLE) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(OidcEvents.Cancel) - val loadingState = awaitItem() - assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading) - val finalState = awaitItem() - assertThat(finalState.requestState).isEqualTo(AsyncAction.Failure(A_THROWABLE)) - // Note: in real life I do not think this can happen, and the app should not block the user. - } - } - - @Test - fun `present - user cancels from webview`() = runTest { - val presenter = OidcPresenter( - A_OIDC_DATA, - FakeMatrixAuthenticationService(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.GoBack)) - val loadingState = awaitItem() - assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading) - val finalState = awaitItem() - assertThat(finalState.requestState).isEqualTo(AsyncAction.Success(Unit)) - } - } - - @Test - fun `present - login success`() = runTest { - val presenter = OidcPresenter( - A_OIDC_DATA, - FakeMatrixAuthenticationService(), - ) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL"))) - val loadingState = awaitItem() - assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading) - // In this case, no success, the session is created and the node get destroyed. - } - } - - @Test - fun `present - login error`() = runTest { - val authenticationService = FakeMatrixAuthenticationService() - val presenter = OidcPresenter( - A_OIDC_DATA, - authenticationService, - ) - authenticationService.givenLoginError(A_THROWABLE) - moleculeFlow(RecompositionMode.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(OidcEvents.OidcActionEvent(OidcAction.Success("A_URL"))) - val loadingState = awaitItem() - assertThat(loadingState.requestState).isEqualTo(AsyncAction.Loading) - val errorState = awaitItem() - assertThat(errorState.requestState).isEqualTo(AsyncAction.Failure(A_THROWABLE)) - errorState.eventSink.invoke(OidcEvents.ClearError) - val finalState = awaitItem() - assertThat(finalState.requestState).isEqualTo(AsyncAction.Uninitialized) - } - } -} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt index 2e56c45395..119eda3423 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.push.impl.notifications 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.SessionId @@ -54,13 +55,13 @@ class DefaultCallNotificationEventResolver @Inject constructor( sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean - ): Result = runCatching { + ): Result = runCatchingExceptions { val content = notificationData.content as? NotificationContent.MessageLike.CallNotify ?: throw ResolvingException("content is not a call notify") val previousRingingCallStatus = appForegroundStateService.hasRingingCall.value // We need the sync service working to get the updated room info - val isRoomCallActive = runCatching { + val isRoomCallActive = runCatchingExceptions { if (content.type == CallNotifyType.RING) { appForegroundStateService.updateHasRingingCall(true) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt index 6ee03ed418..d5eef83564 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt @@ -11,6 +11,8 @@ import android.content.Context import android.net.Uri import androidx.core.content.FileProvider import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.extensions.mapCatchingExceptions +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext @@ -90,7 +92,7 @@ class DefaultNotifiableEventResolver @Inject constructor( val ids = notificationEventRequests.groupBy { it.roomId }.mapValues { (_, value) -> value.map { it.eventId } } // TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event - val notifications = client.notificationService().getNotifications(ids).mapCatching { map -> + val notifications = client.notificationService().getNotifications(ids).mapCatchingExceptions { map -> map.mapValues { (_, notificationData) -> notificationData.asNotifiableEvent(client, sessionId) } @@ -112,7 +114,7 @@ class DefaultNotifiableEventResolver @Inject constructor( private suspend fun NotificationData.asNotifiableEvent( client: MatrixClient, userId: SessionId, - ): Result = runCatching { + ): Result = runCatchingExceptions { when (val content = this.content) { is NotificationContent.MessageLike.RoomMessage -> { val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index f485ba2954..e50536b313 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.EventId @@ -48,6 +49,7 @@ class DefaultNotificationDrawerManager @Inject constructor( private val notificationManager: NotificationManagerCompat, private val notificationRenderer: NotificationRenderer, private val appNavigationStateService: AppNavigationStateService, + @AppCoroutineScope coroutineScope: CoroutineScope, private val matrixClientProvider: MatrixClientProvider, private val imageLoaderHolder: ImageLoaderHolder, @@ -208,7 +210,7 @@ class DefaultNotificationDrawerManager @Inject constructor( private suspend fun MatrixClient.getSafeUserProfile(): MatrixUser { return tryOrNull( - onError = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") }, + onException = { Timber.tag(loggerTag.value).e(it, "Unable to retrieve info for user ${sessionId.value}") }, operation = { val profile = getUserProfile().getOrNull() // displayName cannot be empty else NotificationCompat.MessagingStyle() will crash diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt index 594575bc44..fab6e3a53a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandler.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.push.impl.notifications import android.content.Intent import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -36,6 +37,7 @@ import javax.inject.Inject private val loggerTag = LoggerTag("NotificationBroadcastReceiverHandler", LoggerTag.NotificationLoggerTag) class NotificationBroadcastReceiverHandler @Inject constructor( + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, private val matrixClientProvider: MatrixClientProvider, private val sessionPreferencesStore: SessionPreferencesStoreFactory, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationMediaRepo.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationMediaRepo.kt index 41dd391470..5483c76578 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationMediaRepo.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationMediaRepo.kt @@ -11,6 +11,7 @@ import com.squareup.anvil.annotations.ContributesBinding import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.matrix.api.MatrixClient @@ -85,7 +86,7 @@ class DefaultNotificationMediaRepo @AssistedInject constructor( source = mediaSource, mimeType = mimeType, filename = filename, - ).mapCatching { + ).mapCatchingExceptions { it.use { mediaFile -> val dest = cachedFile.apply { parentFile?.mkdirs() } if (mediaFile.persist(dest.path)) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResolverQueue.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResolverQueue.kt index 1009cc3d36..a3ba36cfe8 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResolverQueue.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResolverQueue.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.push.impl.notifications import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId @@ -34,6 +35,7 @@ import kotlin.time.Duration.Companion.milliseconds @SingleIn(AppScope::class) class NotificationResolverQueue @Inject constructor( private val notifiableEventResolver: NotifiableEventResolver, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, ) { companion object { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index 19b060d36b..a426cdcf42 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.push.impl.history.PushHistoryService import io.element.android.libraries.push.impl.history.onDiagnosticPush @@ -58,6 +59,7 @@ class DefaultPushHandler @Inject constructor( private val notificationChannels: NotificationChannels, private val pushHistoryService: PushHistoryService, private val resolverQueue: NotificationResolverQueue, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, ) : PushHandler { init { @@ -73,21 +75,34 @@ class DefaultPushHandler @Inject constructor( for (request in requests) { // Log the result of the push notification event val result = resolvedEvents[request] - if (result?.isSuccess == true) { - pushHistoryService.onSuccess( - providerInfo = request.providerInfo, - eventId = request.eventId, - roomId = request.roomId, - sessionId = request.sessionId, - comment = "Push handled successfully", - ) - } else { + if (result == null) { pushHistoryService.onUnableToResolveEvent( providerInfo = request.providerInfo, eventId = request.eventId, roomId = request.roomId, sessionId = request.sessionId, - reason = "Push not handled", + reason = "Push not handled: no result found for request", + ) + } else { + result.fold( + onSuccess = { + pushHistoryService.onSuccess( + providerInfo = request.providerInfo, + eventId = request.eventId, + roomId = request.roomId, + sessionId = request.sessionId, + comment = "Push handled successfully", + ) + }, + onFailure = { exception -> + pushHistoryService.onUnableToResolveEvent( + providerInfo = request.providerInfo, + eventId = request.eventId, + roomId = request.roomId, + sessionId = request.sessionId, + reason = exception.message ?: exception.javaClass.simpleName, + ) + } ) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt index 14cb3d8ac6..b311aab4e2 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt @@ -9,6 +9,7 @@ package io.element.android.libraries.push.impl.push import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent @@ -23,6 +24,7 @@ interface OnNotifiableEventReceived { @ContributesBinding(AppScope::class) class DefaultOnNotifiableEventReceived @Inject constructor( private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager, + @AppCoroutineScope private val coroutineScope: CoroutineScope, private val syncOnNotifiableEvent: SyncOnNotifiableEvent, ) : OnNotifiableEventReceived { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt index 8cf77b1898..991baba404 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnRedactedEventReceived.kt @@ -17,6 +17,7 @@ import androidx.core.text.inSpans import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.push.impl.notifications.ActiveNotificationsProvider import io.element.android.libraries.push.impl.notifications.NotificationDisplayer import io.element.android.libraries.push.impl.notifications.factories.DefaultNotificationCreator @@ -36,6 +37,7 @@ interface OnRedactedEventReceived { class DefaultOnRedactedEventReceived @Inject constructor( private val activeNotificationsProvider: ActiveNotificationsProvider, private val notificationDisplayer: NotificationDisplayer, + @AppCoroutineScope private val coroutineScope: CoroutineScope, @ApplicationContext private val context: Context, private val stringProvider: StringProvider, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTest.kt index 01f728d23c..ee140e7f6c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTest.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/NotificationTest.kt @@ -63,6 +63,7 @@ class NotificationTest @Inject constructor( notificationClickHandler.state.first() Timber.d("Notification clicked!") } + @Suppress("RunCatchingNotAllowed") runCatching { withTimeout(30.seconds) { job.join() diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt index 3285ecea10..3f392a3a17 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/troubleshoot/PushLoopbackTest.kt @@ -76,6 +76,7 @@ class PushLoopbackTest @Inject constructor( job.cancel() return } + @Suppress("RunCatchingNotAllowed") runCatching { withTimeout(10.seconds) { completable.await() diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt index ac53bace79..3fd94863a1 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBroadcastReceiverHandlerTest.kt @@ -8,16 +8,15 @@ package io.element.android.libraries.push.impl.notifications import android.content.Intent -import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.room.IntentionalMention +import io.element.android.libraries.matrix.api.room.RoomInfo import io.element.android.libraries.matrix.api.room.message.ReplyParameters import io.element.android.libraries.matrix.api.room.message.replyInThread -import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE @@ -224,7 +223,12 @@ class NotificationBroadcastReceiverHandlerTest { getLambda = getLambda ) val clearMessagesForRoomLambda = lambdaRecorder { _, _ -> } - val joinedRoom = FakeJoinedRoom() + val markAsReadResult = lambdaRecorder> { Result.success(Unit) } + val joinedRoom = FakeJoinedRoom( + baseRoom = FakeBaseRoom( + markAsReadResult = markAsReadResult, + ), + ) val fakeNotificationCleaner = FakeNotificationCleaner( clearMessagesForRoomLambda = clearMessagesForRoomLambda, ) @@ -243,7 +247,7 @@ class NotificationBroadcastReceiverHandlerTest { clearMessagesForRoomLambda.assertions() .isCalledOnce() .with(value(A_SESSION_ID), value(A_ROOM_ID)) - assertThat(joinedRoom.baseRoom.markAsReadCalls).isEqualTo(listOf(expectedReceiptType)) + markAsReadResult.assertions().isCalledOnce().with(value(expectedReceiptType)) } @Test @@ -258,7 +262,7 @@ class NotificationBroadcastReceiverHandlerTest { @Test fun `Test join room`() = runTest { - val joinRoom = lambdaRecorder> { _ -> Result.success(null) } + val joinRoom = lambdaRecorder> { _ -> Result.success(null) } val clearMembershipNotificationForRoomLambda = lambdaRecorder { _, _ -> } val fakeNotificationCleaner = FakeNotificationCleaner( clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda, @@ -467,7 +471,7 @@ class NotificationBroadcastReceiverHandlerTest { private fun TestScope.createNotificationBroadcastReceiverHandler( joinedRoom: FakeJoinedRoom? = FakeJoinedRoom(), - joinRoom: (RoomId) -> Result = { lambdaError() }, + joinRoom: (RoomId) -> Result = { lambdaError() }, matrixClient: MatrixClient? = FakeMatrixClient().apply { givenGetRoomResult(A_ROOM_ID, joinedRoom) joinRoomLambda = joinRoom diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenRotator.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenRotator.kt index d648e3475f..cf6eb4b2cc 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenRotator.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTokenRotator.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.pushproviders.firebase import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope import javax.inject.Inject @@ -24,7 +25,7 @@ class DefaultFirebaseTokenRotator @Inject constructor( private val firebaseTokenGetter: FirebaseTokenGetter, ) : FirebaseTokenRotator { override suspend fun rotate(): Result { - return runCatching { + return runCatchingExceptions { firebaseTokenDeleter.delete() firebaseTokenGetter.get() } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt index 5ed3fb9580..8326496dd2 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseTroubleshooter.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.pushproviders.firebase import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.di.AppScope import javax.inject.Inject @@ -24,7 +25,7 @@ class DefaultFirebaseTroubleshooter @Inject constructor( private val firebaseTokenGetter: FirebaseTokenGetter, ) : FirebaseTroubleshooter { override suspend fun troubleshoot(): Result { - return runCatching { + return runCatchingExceptions { val token = firebaseTokenGetter.get() newTokenHandler.handle(token) } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt index d7c7777168..2c138c3cb6 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt @@ -11,6 +11,7 @@ import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.pushproviders.api.PushHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -23,6 +24,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { @Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler @Inject lateinit var pushParser: FirebasePushParser @Inject lateinit var pushHandler: PushHandler + @AppCoroutineScope @Inject lateinit var coroutineScope: CoroutineScope override fun onCreate() { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt index 5df7c1e5c3..862212c333 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt @@ -35,6 +35,7 @@ class DefaultRegisterUnifiedPushUseCase @Inject constructor( // VectorUnifiedPushMessagingReceiver.onNewEndpoint UnifiedPush.register(context = context, instance = clientSecret) // Wait for VectorUnifiedPushMessagingReceiver.onNewEndpoint to proceed + @Suppress("RunCatchingNotAllowed") return runCatching { withTimeout(30.seconds) { val result = endpointRegistrationHandler.state diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt index a6fbd7051f..4b80ae42ff 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt @@ -36,7 +36,7 @@ class DefaultUnifiedPushGatewayResolver @Inject constructor( ) : UnifiedPushGatewayResolver { override suspend fun getGateway(endpoint: String): UnifiedPushGatewayResolverResult { val url = tryOrNull( - onError = { Timber.tag("DefaultUnifiedPushGatewayResolver").d(it, "Cannot parse endpoint as an URL") } + onException = { Timber.tag("DefaultUnifiedPushGatewayResolver").d(it, "Cannot parse endpoint as an URL") } ) { URL(endpoint) } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index 9cb7e8e60f..b19aa985fe 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt @@ -11,6 +11,7 @@ import android.content.Context import android.content.Intent import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult @@ -34,6 +35,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { @Inject lateinit var unifiedPushGatewayUrlResolver: UnifiedPushGatewayUrlResolver @Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler @Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler + @AppCoroutineScope @Inject lateinit var coroutineScope: CoroutineScope override fun onReceive(context: Context, intent: Intent) { diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt index e35768ed6c..71fc9b6a98 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/observer/DefaultSessionObserver.kt @@ -11,6 +11,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.observer.SessionListener import io.element.android.libraries.sessionstorage.api.observer.SessionObserver @@ -28,6 +29,7 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultSessionObserver @Inject constructor( private val sessionStore: SessionStore, + @AppCoroutineScope private val coroutineScope: CoroutineScope, private val dispatchers: CoroutineDispatchers, ) : SessionObserver { diff --git a/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSourceTest.kt b/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSourceTest.kt index 515aab8072..43dcb30836 100644 --- a/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSourceTest.kt +++ b/libraries/usersearch/impl/src/test/kotlin/io/element/android/libraries/usersearch/impl/MatrixUserListDataSourceTest.kt @@ -50,7 +50,7 @@ internal class MatrixUserListDataSourceTest { val matrixClient = FakeMatrixClient() matrixClient.givenSearchUsersResult( searchTerm = "test", - result = Result.failure(Throwable("Ruhroh")) + result = Result.failure(RuntimeException("Ruhroh")) ) val dataSource = MatrixUserListDataSource(matrixClient) @@ -76,7 +76,7 @@ internal class MatrixUserListDataSourceTest { val matrixClient = FakeMatrixClient() matrixClient.givenGetProfileResult( userId = A_USER_ID, - result = Result.failure(Throwable("Ruhroh")) + result = Result.failure(RuntimeException("Ruhroh")) ) val dataSource = MatrixUserListDataSource(matrixClient) diff --git a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt index 726fccdca0..e6d596e90a 100644 --- a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt +++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/DefaultVoiceMessagePresenterFactory.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.voiceplayer.impl import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.voiceplayer.api.VoiceMessagePresenterFactory @@ -22,7 +23,8 @@ import kotlin.time.Duration @ContributesBinding(RoomScope::class) class DefaultVoiceMessagePresenterFactory @Inject constructor( private val analyticsService: AnalyticsService, - private val scope: CoroutineScope, + @SessionCoroutineScope + private val sessionCoroutineScope: CoroutineScope, private val voiceMessagePlayerFactory: VoiceMessagePlayer.Factory, ) : VoiceMessagePresenterFactory { override fun createVoiceMessagePresenter( @@ -41,7 +43,7 @@ class DefaultVoiceMessagePresenterFactory @Inject constructor( return VoiceMessagePresenter( analyticsService = analyticsService, - scope = scope, + sessionCoroutineScope = sessionCoroutineScope, player = player, eventId = eventId, duration = duration, diff --git a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessageMediaRepo.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessageMediaRepo.kt index 922751852f..9f000b8c32 100644 --- a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessageMediaRepo.kt +++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessageMediaRepo.kt @@ -11,6 +11,7 @@ import com.squareup.anvil.annotations.ContributesBinding import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.media.MatrixMediaLoader @@ -80,7 +81,7 @@ class DefaultVoiceMessageMediaRepo @AssistedInject constructor( source = mediaSource, mimeType = mimeType, filename = filename, - ).mapCatching { + ).mapCatchingExceptions { it.use { mediaFile -> val dest = cachedFile.apply { parentFile?.mkdirs() } if (mediaFile.persist(dest.path)) { diff --git a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePlayer.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePlayer.kt index 275abebdf5..e57f065f06 100644 --- a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePlayer.kt +++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePlayer.kt @@ -8,6 +8,7 @@ package io.element.android.libraries.voiceplayer.impl import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.extensions.mapCatchingExceptions import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId @@ -180,7 +181,7 @@ class DefaultVoiceMessagePlayer( }.distinctUntilChanged() override suspend fun prepare(): Result = if (eventId != null) { - repo.getMediaFile().mapCatching { mediaFile -> + repo.getMediaFile().mapCatchingExceptions { mediaFile -> val state = internalState.value mediaPlayer.setMedia( uri = mediaFile.path, diff --git a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt index 85492fe674..c7811f6d17 100644 --- a/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt +++ b/libraries/voiceplayer/impl/src/main/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenter.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.core.extensions.flatMap +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.ui.utils.time.formatShort import io.element.android.libraries.voiceplayer.api.VoiceMessageEvents @@ -30,7 +31,7 @@ import kotlin.time.Duration.Companion.milliseconds class VoiceMessagePresenter( private val analyticsService: AnalyticsService, - private val scope: CoroutineScope, + private val sessionCoroutineScope: CoroutineScope, private val player: VoiceMessagePlayer, private val eventId: EventId?, private val duration: Duration, @@ -91,7 +92,7 @@ class VoiceMessagePresenter( } else if (playerState.isReady) { player.play() } else { - scope.launch { + sessionCoroutineScope.launch { play.runUpdatingState( errorTransform = { analyticsService.trackError( @@ -101,7 +102,7 @@ class VoiceMessagePresenter( }, ) { player.prepare().flatMap { - runCatching { player.play() } + runCatchingExceptions { player.play() } } } } diff --git a/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt index c10b263b75..9d16cd6bcc 100644 --- a/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt +++ b/libraries/voiceplayer/impl/src/test/kotlin/io/element/android/libraries/voiceplayer/impl/VoiceMessagePresenterTest.kt @@ -236,7 +236,7 @@ fun TestScope.createVoiceMessagePresenter( mediaSource: MediaSource = MediaSource(contentUri), ) = VoiceMessagePresenter( analyticsService = analyticsService, - scope = this, + sessionCoroutineScope = this, player = DefaultVoiceMessagePlayer( mediaPlayer = mediaPlayer, voiceMessageMediaRepoFactory = { _, _, _ -> voiceMessageMediaRepo }, diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorder.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorder.kt index 64f57b0574..5e690ab1f7 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorder.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorder.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.childScope import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.voicerecorder.api.VoiceRecorder import io.element.android.libraries.voicerecorder.api.VoiceRecorderState import io.element.android.libraries.voicerecorder.impl.audio.Audio @@ -51,10 +52,11 @@ class DefaultVoiceRecorder @Inject constructor( private val config: AudioConfig, private val fileConfig: VoiceFileConfig, private val audioLevelCalculator: AudioLevelCalculator, - appCoroutineScope: CoroutineScope, + @SessionCoroutineScope + sessionCoroutineScope: CoroutineScope, ) : VoiceRecorder { private val voiceCoroutineScope by lazy { - appCoroutineScope.childScope(dispatchers.io, "VoiceRecorder-${UUID.randomUUID()}") + sessionCoroutineScope.childScope(dispatchers.io, "VoiceRecorder-${UUID.randomUUID()}") } private var outputFile: File? = null diff --git a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt index 7e7fc51d71..a64d273379 100644 --- a/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt +++ b/libraries/voicerecorder/impl/src/main/kotlin/io/element/android/libraries/voicerecorder/impl/audio/DefaultEncoder.kt @@ -48,7 +48,6 @@ class DefaultEncoder @Inject constructor( override fun release() { encoder?.release() - ?: Timber.w("Can't release encoder that is not initialized") encoder = null } } diff --git a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorderTest.kt b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorderTest.kt index 639686fbf8..6ed540a2f4 100644 --- a/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorderTest.kt +++ b/libraries/voicerecorder/impl/src/test/kotlin/io/element/android/libraries/voicerecorder/impl/DefaultVoiceRecorderTest.kt @@ -141,7 +141,7 @@ class DefaultVoiceRecorderTest { fileConfig = fileConfig, fileManager = FakeVoiceFileManager(fakeFileSystem, fileConfig, FILE_ID), audioLevelCalculator = FakeAudioLevelCalculator(), - appCoroutineScope = backgroundScope, + sessionCoroutineScope = backgroundScope, ) } diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index f171ce669b..cd0dcac4c3 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -32,7 +32,7 @@ private const val versionYear = 25 private const val versionMonth = 6 // Note: must be in [0,99] -private const val versionReleaseNumber = 0 +private const val versionReleaseNumber = 1 object Versions { const val VERSION_CODE = (2000 + versionYear) * 10_000 + versionMonth * 100 + versionReleaseNumber diff --git a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt index 5ee74860aa..31d52d2249 100644 --- a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt +++ b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt @@ -14,6 +14,7 @@ import im.vector.app.features.analytics.plan.SuperProperties import im.vector.app.features.analytics.plan.UserProperties import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.sessionstorage.api.observer.SessionListener import io.element.android.libraries.sessionstorage.api.observer.SessionObserver import io.element.android.services.analytics.api.AnalyticsService @@ -34,6 +35,7 @@ class DefaultAnalyticsService @Inject constructor( private val analyticsProviders: Set<@JvmSuppressWildcards AnalyticsProvider>, private val analyticsStore: AnalyticsStore, // private val lateInitUserPropertiesFactory: LateInitUserPropertiesFactory, + @AppCoroutineScope private val coroutineScope: CoroutineScope, private val sessionObserver: SessionObserver, ) : AnalyticsService, SessionListener { diff --git a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt index f173812fed..204ab66495 100644 --- a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt +++ b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt @@ -11,6 +11,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SpaceId @@ -36,7 +37,8 @@ private val loggerTag = LoggerTag("Navigation") @SingleIn(AppScope::class) class DefaultAppNavigationStateService @Inject constructor( private val appForegroundStateService: AppForegroundStateService, - private val coroutineScope: CoroutineScope, + @AppCoroutineScope + coroutineScope: CoroutineScope, ) : AppNavigationStateService { private val state = MutableStateFlow( AppNavigationState( diff --git a/settings.gradle.kts b/settings.gradle.kts index e594999bd8..f523b72fec 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -56,6 +56,7 @@ include(":appnav") include(":appconfig") include(":appicon:element") include(":appicon:enterprise") +include(":tests:detekt-rules") include(":tests:konsist") include(":tests:uitests") include(":tests:testutils") diff --git a/tests/detekt-rules/.gitignore b/tests/detekt-rules/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/tests/detekt-rules/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/tests/detekt-rules/build.gradle.kts b/tests/detekt-rules/build.gradle.kts new file mode 100644 index 0000000000..7fbcdb3a8a --- /dev/null +++ b/tests/detekt-rules/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(libs.plugins.kotlin.jvm) +} +java { + sourceCompatibility = Versions.javaVersion + targetCompatibility = Versions.javaVersion +} + +kotlin { + jvmToolchain { + languageVersion = Versions.javaLanguageVersion + } +} + +dependencies { + compileOnly(libs.test.detekt.api) + testImplementation(libs.test.detekt.test) + + testImplementation(libs.test.truth) +} diff --git a/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ElementRuleSetProvider.kt b/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ElementRuleSetProvider.kt new file mode 100644 index 0000000000..8e0ed3ee50 --- /dev/null +++ b/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ElementRuleSetProvider.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.detektrules + +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.RuleSet +import io.gitlab.arturbosch.detekt.api.RuleSetProvider + +class ElementRuleSetProvider : RuleSetProvider { + override val ruleSetId: String = "ElementXRules" + + override fun instance(config: Config): RuleSet = RuleSet( + id = ruleSetId, + rules = listOf( + RunCatchingRule(config), + ) + ) +} diff --git a/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/RunCatchingRule.kt b/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/RunCatchingRule.kt new file mode 100644 index 0000000000..094de51be3 --- /dev/null +++ b/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/RunCatchingRule.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.detektrules + +import io.gitlab.arturbosch.detekt.api.CodeSmell +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.Debt +import io.gitlab.arturbosch.detekt.api.Entity +import io.gitlab.arturbosch.detekt.api.Issue +import io.gitlab.arturbosch.detekt.api.Rule +import io.gitlab.arturbosch.detekt.api.Severity +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.psiUtil.getCallNameExpression + +class RunCatchingRule(config: Config) : Rule(config) { + override val issue: Issue = Issue( + id = "RunCatchingNotAllowed", + severity = Severity.Style, + description = "Avoid using `runCatching`, use `runCatchingExceptions` or `tryOrNull` instead. " + + "Avoid `mapCatching`, use `mapCatchingExceptions` instead.", + debt = Debt.FIVE_MINS, + ) + + override fun visitCallExpression(expression: KtCallExpression) { + super.visitCallExpression(expression) + + val callNameExpression = expression.getCallNameExpression() ?: return + val hasRunCatchingCall = callNameExpression.text == "runCatching" + val hasMapCatchingCall = callNameExpression.text == "mapCatching" + if (hasRunCatchingCall || hasMapCatchingCall) { + report(CodeSmell( + issue = issue, + entity = Entity.from(expression), + message = "Use `runCatchingExceptions` or `tryOrNull` instead of `runCatching`. Avoid `mapCatching`, use `mapCatchingExceptions` instead." + )) + } + } +} diff --git a/tests/detekt-rules/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider b/tests/detekt-rules/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider new file mode 100644 index 0000000000..454797e7a0 --- /dev/null +++ b/tests/detekt-rules/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider @@ -0,0 +1 @@ +io.element.android.detektrules.ElementRuleSetProvider diff --git a/tests/detekt-rules/src/test/kotlin/io/element/android/detektrules/RunCatchingRuleTest.kt b/tests/detekt-rules/src/test/kotlin/io/element/android/detektrules/RunCatchingRuleTest.kt new file mode 100644 index 0000000000..c11d4d8868 --- /dev/null +++ b/tests/detekt-rules/src/test/kotlin/io/element/android/detektrules/RunCatchingRuleTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.detektrules + +import com.google.common.truth.Truth.assertThat +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.test.compileAndLint +import org.junit.Test + +class RunCatchingRuleTest { + private val subject = RunCatchingRule(Config.empty) + + @Test + fun `test RunCatchingRule`() { + val findings = subject.compileAndLint(code) + assertThat(findings).hasSize(3) + } + + private val code = """ + object Foo { + fun bar() { + runCatching {} + kotlin.runCatching {} + Result.success(true).mapCatching { false } + } + } + """.trimIndent() +} diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt index 020245b906..1bcf7bbfca 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt @@ -15,6 +15,7 @@ import com.lemonappdev.konsist.api.ext.list.withAllParentsOf import com.lemonappdev.konsist.api.ext.list.withAnnotationNamed import com.lemonappdev.konsist.api.ext.list.withNameContaining import com.lemonappdev.konsist.api.ext.list.withNameEndingWith +import com.lemonappdev.konsist.api.ext.list.withPackage import com.lemonappdev.konsist.api.ext.list.withoutName import com.lemonappdev.konsist.api.ext.list.withoutNameStartingWith import com.lemonappdev.konsist.api.verify.assertEmpty @@ -89,9 +90,9 @@ class KonsistClassNameTest { ) .assertTrue { val interfaceName = it.name - .replace("FakeRust", "") + .replace("FakeFfi", "") .replace("Fake", "") - val result = (it.name.startsWith("Fake") || it.name.startsWith("FakeRust")) && + val result = it.name.startsWith("Fake") && it.parents().any { parent -> val parentName = parent.name.replace(".", "") parentName == interfaceName @@ -106,6 +107,17 @@ class KonsistClassNameTest { assertThat(failingCases).isEqualTo(failingCasesList.size) } + @Test + fun `All Classes that override a class from the Ffi layer must have 'FakeFfi' prefix`() { + Konsist.scopeFromTest() + .classes() + .withPackage("io.element.android.libraries.matrix.impl.fixtures.fakes") + .assertTrue { klass -> + val parentName = klass.parents().firstOrNull()?.name.orEmpty() + klass.name == "FakeFfi$parentName" + } + } + @Test fun `Class implementing interface should have name not end with 'Impl' but start with 'Default'`() { Konsist.scopeFromProject() diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts index a7b81808f9..8a7730095a 100644 --- a/tests/testutils/build.gradle.kts +++ b/tests/testutils/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) implementation(projects.libraries.core) + implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) implementation(projects.services.toolbox.api) implementation(libs.test.turbine) diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt index 0723d85d65..620b5def40 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RobolectricDispatcherCleaner.kt @@ -9,7 +9,9 @@ package io.element.android.tests.testutils import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import io.element.android.libraries.designsystem.utils.LocalUiTestMode import org.junit.Assert.assertFalse import org.junit.rules.TestRule import kotlin.coroutines.CoroutineContext @@ -49,7 +51,16 @@ object RobolectricDispatcherCleaner { } } -fun AndroidComposeTestRule.setSafeContent(content: @Composable () -> Unit) { - RobolectricDispatcherCleaner.clearAndroidUiDispatcher() - setContent(content) +fun AndroidComposeTestRule.setSafeContent( + clearAndroidUiDispatcher: Boolean = false, + content: @Composable () -> Unit, +) { + if (clearAndroidUiDispatcher) { + RobolectricDispatcherCleaner.clearAndroidUiDispatcher() + } + setContent { + CompositionLocalProvider(LocalUiTestMode provides true) { + content() + } + } } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Error.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Error.kt index b96900cee1..f9e2ec0cbb 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Error.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Error.kt @@ -7,19 +7,8 @@ package io.element.android.tests.testutils.lambda -import kotlin.system.exitProcess - fun lambdaError( message: String = "This lambda should never be called." ): Nothing { - // Throwing an exception here is not enough, it can be caught. - // Instead exit the process to make sure the test fails. - // The error will be: - // "Could not stop all services." - // In this case, put a breakpoint here and run the test in debug mode to identify which lambda is failing. - System.err.println(message) - Thread.currentThread().stackTrace.forEach { - System.err.println(it) - } - exitProcess(1) + throw AssertionError(message) } diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png index e27e1c9eda..446ca5cbff 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e57fb5dd99713d410b16fdb8b90a549216387c9213395f54aff8e48ac843e97c -size 233859 +oid sha256:3c3a0afd6986943481bff9de80e38a603b0682e8d391da52859cb7691f3c47ca +size 612888 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png index 815099ca57..8aef2143fd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e7f7cb7e70b11d423297ccb40eb25c31f99e7fb6d736faa60891f99d5f679ad -size 234026 +oid sha256:9d8f46b60243bab1c969723c14bb43020fb84cad4a8c69e7cc2712e2306efc96 +size 611660 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_0_en.png index eabb733771..c151d17d1d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ec7630cd05ce94f7163f0aa6762378080c35f04a1b59ce2b285e6dfa8e12129 -size 111484 +oid sha256:5403784952c6fddee0c59906399330d9f31f58f128f4320bf72a7f26196213b6 +size 278427 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_1_en.png index 2080330b56..42704f5647 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92984da13001f38360a2871de6d0cd84bad33aac91059a6ed9e784cd701ad623 -size 166389 +oid sha256:871cede89b11408db3873b7512f204a30c10461e5182ed1dfc0ccbf459e5ae86 +size 509431 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_2_en.png index 55399a703f..be7dad6a73 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3619ebab31f741909dc550fbc0a89b143b1edaeb395379314228ab490b726910 -size 121439 +oid sha256:0df7697308d09639445cfeeb3b11ce770442b9e5d05245823b265eee75f34ecf +size 386760 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en.png index 1b6fb4bab8..c151d17d1d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 -size 3642 +oid sha256:5403784952c6fddee0c59906399330d9f31f58f128f4320bf72a7f26196213b6 +size 278427 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_0_en.png index b4a3a86728..09e6c57d8e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7de6b943582cc341aebd7dcf553d51b3284414e988918267e7e2ca7585c36bec -size 111996 +oid sha256:1d6888c7a0170c9d5b413f84e9e9b649fd99b89584f0f75c33e2602184236313 +size 277274 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_1_en.png index 6e2d5f83af..bdf2a68f05 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad066040b511f4270b6b4b05b7043f451d5d8b3cf5b5526aa34c26e5acc8ca01 -size 166511 +oid sha256:c2f098a79f6aa516c3624481c573b730de1d978616068ca2e9994850922a1172 +size 508910 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_2_en.png index 6df8e4e675..392d24ca1c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8eb833ac28952ec172d315aca358f44e34984611d98ab1f146384b2b94bd678 -size 121980 +oid sha256:00d6355d80faafa3d23e9048d5fe7f2e8a72259a2dd548e1437f97167b2a22e7 +size 386591 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en.png index d6fd8eeb70..09e6c57d8e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemImageView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd -size 3659 +oid sha256:1d6888c7a0170c9d5b413f84e9e9b649fd99b89584f0f75c33e2602184236313 +size 277274 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en.png index 40ad608500..1a37bb193d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bee6962a35cc2da956be0488f446c9a3d8831cec97f30e0828440068f764a0d1 -size 34241 +oid sha256:543d5ae9fdb81983d787f6712f6478a5dca7cfcc3091d5879182978488a0dc89 +size 55150 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_1_en.png index f1d9752b9e..bec0c7c32a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f08bf0cad0c94da75a0efb97dc92bc3e3c36cde4d833a6a26f9b0d63887785bf -size 47453 +oid sha256:67bdc28eff7830debb89f73c859538f45c73c8d3318414a54eadc14a9426d8e4 +size 101874 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_2_en.png index 4238d4bab8..18f4e77293 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d5253e52390f9dcdd46db4f8d98d26be7cc8d05176d707a8a8c10adf4e7307b -size 57064 +oid sha256:3db1d6d23850cbc6ae15b4942b6d10d5c9a65acb1d0d73f35f91579386b94d53 +size 147057 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_3_en.png index 1b6fb4bab8..1a37bb193d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 -size 3642 +oid sha256:543d5ae9fdb81983d787f6712f6478a5dca7cfcc3091d5879182978488a0dc89 +size 55150 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en.png index 1db3339236..c4bfe4172d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d37431be2add3d26b6fd41a94a4c2970d3303de8d39154f1b36f33ec4ae6bf44 -size 34260 +oid sha256:f7e51e8be368bf0b21736f82f2da1aa9526f7a328f760f1108bd23b3bbbd04e2 +size 54911 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_1_en.png index c6c6efe376..e3f05bf82c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a7cdbdd2fac32dcf3c08dcd2abdc8ec96c7b5d56bdfafb5b8594e84ad5da884 -size 47375 +oid sha256:3c446a0219ad13ec361b2eae277e07572e0e13e06b5dbab6c3a6ccbf1508321d +size 101527 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_2_en.png index 94bac0a161..7dde6555d8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0f597867f02b8c31fc094f02b39bea68a00b553cb999e3231f4d046e09da9a0 -size 56893 +oid sha256:43cc19389a22848f730e5f653b82f97581f92f5ba866d351f40a33d24f896923 +size 146574 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_3_en.png index d6fd8eeb70..c4bfe4172d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemStickerView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd -size 3659 +oid sha256:f7e51e8be368bf0b21736f82f2da1aa9526f7a328f760f1108bd23b3bbbd04e2 +size 54911 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_0_en.png index ccb1f6c8df..944db7b863 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47f988423418affc0c1ad48ed1f2780d10ae81288dab0d189e16f66afc418973 -size 112121 +oid sha256:3d484043eaf6e32e5c0895ae56cec62d339b8c3d47acc1a90a8c5a82fdf89ba7 +size 275766 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_1_en.png index eeb9b4e2bd..b069ff0536 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:346a951c1331a6a98196d8be8892e7ca3191d4981f63886ab28ce9dc4143f6d4 -size 167017 +oid sha256:b4a7c9bdf67955f0eb96c4cd59ab16c78b5a46980c104b99cb3ca89a8202386d +size 506789 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_2_en.png index 5e6a13c1e9..cd7348d3d9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b088123524aa44acd5f35877a0e78cfc1d9677fdd6b2af82dd1514a7e44a208 -size 121910 +oid sha256:6233363e62d9dba580117270d8ae3c1286cb132a9c7528a132cdb59f0a61808c +size 383811 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_3_en.png index 5cfbd329ca..944db7b863 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1938d0dd95bde157aae53a7aa665ebcbb3d3d99f90b75307e6970f1aa00b10ce -size 5067 +oid sha256:3d484043eaf6e32e5c0895ae56cec62d339b8c3d47acc1a90a8c5a82fdf89ba7 +size 275766 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_0_en.png index d9baf1b197..e46a37d9a8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eab9dbc99ba34b653b1899eb486ed3401cd6090f3c56e5d06c143eda06bef008 -size 112357 +oid sha256:cb8b28095fab428b0f08646e55532ac6c5531896591c32a27d2db0903575ddfc +size 274731 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_1_en.png index 8adf6a36bc..f64da45b54 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5485bbd279eef694c1d9f3582e47e35e2a45f3799162027bdb3ae108e421d1ec -size 167000 +oid sha256:b6ced0f778044c0438a812399a0f75909eafa74abfebe8c06eb497503493ca12 +size 506418 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_2_en.png index 9533ef9c85..468bb7197d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da705cf7a08d5bd94cd9772767bcf66d2375aeb4e0604dd3319ea2c90670066e -size 122303 +oid sha256:51d821d25096f24a154b369d0000f595212ca65724a2c751c1ff31a44704ae57 +size 383741 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_3_en.png index e8d89691dd..e46a37d9a8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineItemVideoView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:269720ee1d20c09a1c455c6f6b11f7f65772090ac3a0e936294612fd89c88061 -size 4864 +oid sha256:cb8b28095fab428b0f08646e55532ac6c5531896591c32a27d2db0903575ddfc +size 274731 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png index c9967047f0..e899c7d79c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19c15a177a821d705e0b05f0ad028a705a6e62634029ed2233ffc00eee452fda -size 233207 +oid sha256:bb05880b161ebad9cb5c3f78ff217e9ee64e3d6bc07996e32966d9a3f0a2e30c +size 608283 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png index f8cab0b55e..78afe10f31 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83fdfce1407b27d24b941397e764120db1e5e371e1a0cda706cd51c2aba5ab54 -size 234714 +oid sha256:80bca1e6c13e4e345a60b53f6c0479a17aeb05f42f3c13a9dc933e0e6d1d7b3a +size 606934 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png index 22c3782785..9fdcbf7e6c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5bb21840d22553fc7db6252f294a2a05971205bac00506b44a6255e93ba1590 -size 169035 +oid sha256:6693e2e93d21c61c1e3bb6b57bdfca788b8e98aaebf861a5a3ad594344441195 +size 379233 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png index 864e2992db..bac357c0b8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0f667300929a4961a4ac20196a010d001b5eba035086196bfdeb067f4b74f4d -size 168152 +oid sha256:10fafb2e4788ab76d609e74babe55048b44440d8bc6cfd35e78e8337a210b240 +size 377760 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Day_0_en.png index 84d59c2700..9f6c3f4cb7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b8c226a0d0e1026093f1a9b173b7b5c1228e2679ecd01d3da16b3b1ef8dc5d0 -size 167010 +oid sha256:b3d1789882ff07396fb4daf788437744bb71eaa7cca127fadd1bae3cea343116 +size 294216 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Night_0_en.png index 5987ffb7c1..5add18e222 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowForDirectRoom_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bb2568dc80f1aaaad1c425d42476f102876e9103569b38cf2ee5b5c87aee71a -size 166148 +oid sha256:ca042e33785370974388e4949b871c9cd8936f1578f383088c47d12514f8fd46 +size 292986 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png index 5a2dfccf70..f218ef6eff 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31beb2238023043f5b2f7cd2e86ba66f3a24062ea3183b4e9d22025cf2bbc570 -size 152506 +oid sha256:660bd025d6cdd9a9cc43309276c46cf14f2567322d849e7ccf99d1741cece894 +size 377499 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png index 9d3f43358c..18cb5301bb 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowShield_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b539c68c246e5b60a5f0c0cfffa94cf51710ec26e6d5b533790d09dfc9f0bc8e -size 151766 +oid sha256:daf954ba004cd67f85913e8ff4215a0d32bcba8726179ed77bfd8c9557ce3ddc +size 375420 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png index 04b0ea1b72..f100a0416b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52839c50b1ebdf1a46e105045ae03b4ef2cf6a5e1e8cc842c2329e9da395eb81 -size 151404 +oid sha256:192f07e6dd138911a30e5bcebf75a68ed59bad006041e9ba2fad809cd8a94f11 +size 364786 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png index e4f63cc390..f015705293 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b420c048cb72dee9387bb1f8b32e0cda7d703569f67416d35c4f6e0cdfeef0c4 -size 156859 +oid sha256:3e4f5b7139fb1759e9e9c17e0911ab9fff66f4ee14545f2f5977a7d1ef8c7aef +size 369967 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png index 66cb2b3a6c..76decf4e09 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:226ee1a4d618dbfaa6a9fa693af46daaf25dc17fc7fad5190ea2b36473a3963d -size 151407 +oid sha256:4ea40e231b7a23ca3ececa68672f13a7391fbc7fd3719ba98d67e02d6cfe0034 +size 363024 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png index 624b83fc6b..8884e46a51 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9149ea40285c1633b59dc1e1c62a1ffb560bd3f4ce97361044e0be98d59e1e6 -size 156521 +oid sha256:b12cbdf1b95c272f5482c43686ef549801d2c3d5bffd75b6423d6a3225e40efb +size 367997 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png index 54d8ed81b6..c740ebf66f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93d324f26910b61eb576ad8c37fe4bfad79e93879d59f9f3017e7a8c0cef1cfd -size 142620 +oid sha256:582fc51bc3449b2796d3efb0211eaf14dc73fa0ca1817e1d53f29586dfc36d3f +size 353092 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png index 5f923d3618..9f42bbee34 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:338261e41a91279114bf8ee6c5a3ff0c94320706ff3f0d405b17a1ef87eec088 -size 151649 +oid sha256:540912d38358bdea807311e7803397cf402b434f16860ce434aea7751cf2b28e +size 363471 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png index f5f7cbf47a..85ed81e3d8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c9c9e85f84e73951975825beeb355883da4a12f9cdbac0d247736e995857640 -size 141955 +oid sha256:6eb1b7b869f25e0d505ba9ff8dac4e6b63c4f59e7d8854a20233b8d4d77ac1e9 +size 351780 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png index f3ecb9df4c..0b0af20ed0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62a3c3031385ac731b500fc7c8f6d16b58c7f2ca6e4b758b4edb635cc9e6933e -size 150720 +oid sha256:d41539a90d7f82364dc3599423ad6a1b51e53be0e70df926365694b75dae6b8f +size 361273 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png index 6689499dd2..cd30e4d1e8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8c58a09e139e2a2fe9edc1b2bdea0cb399b1a5c1978a19aff2da86f2fd84eac -size 156247 +oid sha256:d086f9df50219d9134d3cab7e4c198d7bac812fe614d2375e80c2e00766d7161 +size 369509 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png index 4e7207012f..84c0c6732a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7a791e9c8788632f03e30aba24af768953411dffabde644bea176cd4811bee5 -size 142208 +oid sha256:9d11576c0e48980bf1505d074a84f11170f042abc92cc80c97b92f5d473588d7 +size 354292 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png index 9293dc99b1..8aa5dacdce 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5571483499c4acee1bf08dca534148159d6b521bceb51c306773593539639bf -size 154286 +oid sha256:77dd4e2eed54c37a022d6a6beab69953cf64fd9725df0b954eb329e647a00036 +size 367658 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png index 5d571bdb7a..afc2b8cbd0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2f1da2796934102bf20655ebe9dd3ea95972399aa77faf13e550b80b3d320b1 -size 163435 +oid sha256:d1312e7e3a6b0ead94a30e0d4971277305469fbbaa2015df55334c37ac1a0136 +size 344733 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png index e40b3c2da8..47dd865024 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4b8b6d80dd685efa4c627c6f48207040561c322dc93d355fe00793981ca88f3 -size 144964 +oid sha256:0000cca03cefb845bcc349669b40abddbb5567c4f7d79481e1d17edbd1b705e8 +size 357300 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png index 1efc0bcccc..d6bb3466d8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0cf792b404583b855f7d09e1535eba11f597e3f4706e367dff00f608aae9029 -size 144176 +oid sha256:c9f8957975959f22f321edb1da337268630b2035670ea6a7f1d0253ceee23904 +size 356520 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png index e4da1ad973..9a01b3a0c8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ce2fcbc8fdc3b6799448c92919ef43b7fc749c2b3b9894bca18e16f8ccd5dd9 -size 150903 +oid sha256:59865d91c14d22ac51f03c5a89f22998aec74251ba61c0db013e549ef71bf724 +size 364285 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png index 805dbaff2a..ecb2198054 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c27a5471ba179e4dd66fdaaae1ec56ac2a86dbf01d62b16c448f5a0df2749519 -size 142566 +oid sha256:1a13e340822f9b7faea91575a5fe8d82a078f7dab567d5f3e573c6090d342d79 +size 396959 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png index 08354b0cae..387dfc2067 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8b4fce8e61026cf00536605f8898505cec9a974aaae1d5951fb79a30bbec95b -size 143360 +oid sha256:9406216df3f9744d7526ef2bfa208abe22796a5e95c1007bcaa6305b7a7e6dd0 +size 355618 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png index ed957abb38..59d432c201 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0c129646f3fd0a78d6c747f575d98696330c82633b119c266c2eea081cb36e8 -size 145193 +oid sha256:b8369665c1e4e9e6e34a44897d6496946beef7563703bbd64b0dc0182b4f81ed +size 357542 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png index ba9f50c1cf..f433fe1b6c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0063fdc45a8ceb4530591b4e02008eb9b754cefd43adb8acac0e754860a46340 -size 151440 +oid sha256:71e21fd5b02aa8ae98519996986e3bd28c37353cfa16fd832f83790a0f2946e5 +size 364744 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png index 29cca5a8ad..a6b63736df 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e53a7ba5131d2174121e72f520a3e9c2aa4b5c389690d2d47cfe419f8ff4290a -size 142838 +oid sha256:dbc3ea0006d57682c5a59ccaf873196b6f5c329d300395935e355e50c9f01906 +size 354991 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png index 138530aaf2..c9f4dd107d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec6a17623cb93cdf165d78c58b06a9a643dff24ebd8cbfbb4bf33096942ad2c8 -size 156098 +oid sha256:cdcbd58a833ac5554260389c9d455aec0bc40ac36bcd378d1a47fd3d3e684b24 +size 367589 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png index 324dc7ecbc..c1d8902cf8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f04df3f019284faa05b6286852276de5b6d7a047cbdfded1303fdd744e7130df -size 141956 +oid sha256:ba5dd0bd237dd41206cfb92cd5d57e7c46710fb532fb447139014b8da811a324 +size 352565 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png index 82f5d83b3f..a127b82fa8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b5d2080791bab646a110e912e98e877347b5b5a601603161e93b7cb1c2f6dd3 -size 154322 +oid sha256:3f4d22986976b525a4f4ba5a6ad5372ffc0c77f6b3dadbe3989cdb98ead2caf3 +size 365876 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png index 3fd5025675..1b81a6a6e4 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdd4ce216c2eb8e0e71509f95407fb832ab8923408ee0d2b7be0ba38abd696d1 -size 162129 +oid sha256:3d551e6c58cebf53fda518877916e13204c52b4db2af3151678546d7b279e14c +size 342627 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png index 55d4ef365a..7330cb98e6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37529421d63a9f7d97887145e91f9f0827574a5bdbe1eda2e085612b451774a3 -size 144878 +oid sha256:93319839a10d070b61501e95bf3a555b493c21d5e328213373fe9d0c888e07e6 +size 355803 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png index 7359f1d853..edc5dd51a7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e01b74cf05faac0b4aabc7848f8780906750a83c2b3ee914bdd6e208c14bfac8 -size 144145 +oid sha256:ac9a160d9e58e3bf44a491e4f1e3c37720ffd91d622729750fb8219428dd2085 +size 355002 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png index 1f8482c849..5176beb76d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ea2c5908d322bae326846ee7e3762a04affc6ff77842951515fc35379f8717f -size 150825 +oid sha256:ae4443f10013aa72805aa09b36688cceb013012424843b00b6eef0496a66fa7f +size 362403 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png index ef5dc390c5..7a41ce872d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5173c0ff87560f9ecb0d5ccbf52992a01a43f53a1665cf565e0c56fb702a07f6 -size 142448 +oid sha256:37e3043ddf1e8fd9db9c156c9cdb88fe236c7f76e166a75871fc388bb448ec07 +size 395179 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png index 2bfc650bbe..f5c45a8b06 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd65c0380b79e7adf032027b8e6aca43465e62bd349a893accbf6e1e3137d39a -size 143054 +oid sha256:2a842b4b9ea2c1b7235444df058d501de4ac57cc7749b30fd91f934502a7429b +size 353792 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png index 1a1168bc58..40f2cae44f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbaa6c97ca79422887365ef77267d78b04c835067873f85df6bead249dec58ee -size 145140 +oid sha256:3b27153819669af47324316fa7b6433b57fc9cf64928de3f9acbfcdd0657d3be +size 356028 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png index 7d06290fac..dad70f4bde 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9332fc228a959263cdc1acaa69a68092a0bd5f5cbc460c1581c775739d633f2 -size 151339 +oid sha256:009b303d3bcf767a4cd58172e5b4f21c1a341ed272ff5ca9f62ef349808740dd +size 362905 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png index dc3e4c115d..0cc82f902d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad635c9076f5d15c37b914fbbffad4bfa4d26a2f13c390f852bb63371730bcbc -size 142574 +oid sha256:434720f8ad2aa0cb2c8fd708cc7467af92e063dc2c3600a3783762e0364592c2 +size 353266 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Day_0_en.png index 6d5033605b..d4e9b0e675 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59ad66398a4b4f7f5f63cda8c15d98a78aff40d2a168bc8fa7cc6df6ae1d823d -size 184170 +oid sha256:ff6e3d41cc1d3a51a4a6d48f47407832d838ed135752e200f588cb12062c18c7 +size 412176 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Night_0_en.png index ab62d8803a..88638ca095 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRow_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d06575021a862195747c35219515a4949a089d65c1c6282c864f2116e3800704 -size 183547 +oid sha256:dfa4dc6a76b73b8b2d48234676de66eed10750ad2c561a065909d4745ab6f4e5 +size 409878 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_2_en.png index c50d921761..1bd497adfd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51c236ea63de8215a9a21107555e0b27902af004e5fef98f304dd1bbde3d9b03 -size 200828 +oid sha256:a8fc7a5830027eb95476df6e378269c1ff37d57c75aac50e3353647d4e84249a +size 492060 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_3_en.png index 78de68a59e..0a12d7dbb6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01ad764325f9362613584c51247d523fa51724be613084a4d3e59a7355df14f0 -size 201982 +oid sha256:949ea4bca530c513ff4e67f4b3b67a69215931ab1fe9bc42b02ed347f02a9bf6 +size 487167 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_2_en.png index eb30365370..a8aa36e1b8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0108d54a2d6f2d5454c94fae5bba903d2dfcae495ae74b7644e429f75a70d3c2 -size 199672 +oid sha256:78ad98cc2dbd846a72b6f481677d0964689ac7f165be662f8b218d6546921dc6 +size 489004 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_3_en.png index 16ece4cc8c..7d0e54cd42 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline_TimelineView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e67ca0306edf3469dbb74ae10e75b69512524169b38d72463408f0b15d8babfa -size 200518 +oid sha256:3aa03a48407e9c8c5214fcc8392e74c2323a73fba8418d7a088575c6756612ad +size 484132 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_en.png index 9454d33fc6..bfe215fb51 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28cbc07493bc3a91f391b0256554171bf60cc02d25ab8d0d78d68168fe62f534 -size 14993 +oid sha256:37a0c2d8d87124b700269ade727cb51c90a85e5549333c44763dfdd29bbcecf8 +size 15007 diff --git a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Night_0_en.png index 844f5b8b3b..8209f29fa4 100644 --- a/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roomdetails.impl.rolesandpermissions.changeroles_PendingMemberRowWithLongName_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bfa988a2f41961dab1fc0ec245188dd18c1cfb430ef24bea5993593215d5f8e5 -size 14734 +oid sha256:006ef95362f37664efb45e7e6378df14d247827652efbea15abfca65ba2bd32e +size 14732 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en.png index db1dc5356e..671153e6b5 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b9222fa30b9d331507f367dfd71108f0c3fafb698d74afa908d9146391660d5 -size 16796 +oid sha256:c023a4169536c12d13c7599c4d92e77269337e499b92da85df6501aa18b7818a +size 16929 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en.png index 2d38dfd46b..3af517ba6b 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0c6367c53677d284694ed3e06f8d548a33464822094445344aa7cbd8bb3e88e -size 20687 +oid sha256:73a35d9dc688e2c992fc53ebafbf547f88fd96d5272ccf58fd1ac8a8fde55ab5 +size 20793 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en.png index 15a24a06d9..82d780aea2 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7fbdf555e12892fa89fa73e77d86f7f2f1beab8328fd0b22498e37ee5f966c46 -size 23648 +oid sha256:b7a86f06886332e666f2aae7db543bde1eee1dc0c193f4c5797c1289f9287707 +size 23764 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en.png index b6be401d79..702f7b1797 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02d7a5e6169a31c546745565e65766cedb4e70faa00ff73675a5781da96cefaa -size 23770 +oid sha256:752e41bf7050d0e21b644503f1fde9dc479e27563aa6257f2b259ca35d05d15b +size 23886 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en.png index e67465ae29..e460132108 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52ae5c7e74cb17dfc9009ff8eb90c9ca4e9b5d198914fbaede56302cede67b7d -size 15660 +oid sha256:a9d8954f9462d07f7e60bad2bdfb350874b203b389433288b22366d54b4e4b1c +size 15707 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en.png index a335623384..3ac9efeaf3 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34432e8a0eaecd71b4f0db3eeb4914a1e0450652d3c4bdc6d57745d530c51128 -size 19280 +oid sha256:049ba622b24940e5bf6b2b008b4ba3752d67b744b816cb8af2909cda56dbf0ad +size 19298 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en.png index 92014e1c1c..5953c6cfc9 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3c8ed9cea0b3525ef565cbd34b3f7014b632f0c3eee9ebfc037d3ae08c44929 -size 22262 +oid sha256:958e47f8f5905562f78fcec67a47d0178b50bc0898d63c03deaeb5bb0ddedd8c +size 22307 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en.png index 52dd3b4301..f61d40f535 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a02fe75a7c5489a1e2020169db6ac50c117db968f111ce863ced5a2f59c08d1 -size 22372 +oid sha256:2ad01b142920b585301210bfde02c0cb942418872d10030de0e8422199169b71 +size 22410 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en.png index 38e1253d41..d7219a131d 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5d78f00d33cad5e00719eec49f6b55959b5c23b3842a0f2abc9280961ebb29d -size 12660 +oid sha256:896715e04f98bd79679081093c3e46321d3e22f0b9e45bb620fe7c28c8e9bf73 +size 12672 diff --git a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en.png index 486dae34af..327cd1193a 100644 --- a/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.verifysession.impl.ui_VerificationUserProfileContent_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4612c8404b671a01129d2334a7aa296218bb4b00820265ef5d00fd9603e7f850 -size 12272 +oid sha256:9fcfb26e81944f1ccfe9144593dbcb5ac2fd6341ce99ac0b268a42e5c435cef7 +size 12287 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_11_en.png index 0cf3a95fce..808d016ec8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e44ac29c970ddab492a1b9550315a82f983b88474287165ba732e57951647a81 -size 19112 +oid sha256:5689b4b10c7225b8eb44b8573a9e7378b2d2673439b24a4d85351d9d7ef6273d +size 19162 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_14_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_14_en.png index 37040e69bc..746e5221a6 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_14_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_14_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:498e1b23a60c563387bbe734fc4ffd3896824d128e1213b194b344091058ae8a -size 21480 +oid sha256:18b2140a2d22c4187d9f338c5d8516b65de738c4ae640b6870f49f0991860e66 +size 21485 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_17_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_17_en.png index eab29dd2b2..e57ce0c297 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_17_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_17_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:206f721ba79222702409f10e70e671e00d855e192387b2fc73dec93bc721765f -size 20933 +oid sha256:cd82839d55fcf1869595428b42b5a7add88199f5c877d6b5dc651c0ad5db2dfb +size 20942 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_20_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_20_en.png index 714308dfa4..922f4bad6d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_20_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_20_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e48c08ee8ff50b9b03bf6af20dfce17db88865b0d1b6db9cbb210d79b8177168 -size 24574 +oid sha256:4c0edbcdb326e027f692feebfa253b23acca3b9a38aa9e2d32ec901afd3ee071 +size 24599 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_23_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_23_en.png index 113f1cc272..f993d1f0bf 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_23_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_23_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64ba8cfebc5697bacedd498a508fe9f95491cb58128cbbbb39fc5a363b56c652 -size 16609 +oid sha256:f25815e696b4df2eab0d6553d8eee57ba2f845a6a959b94f958dbb92817f1696 +size 16611 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_26_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_26_en.png index 4e9d8edab8..257cc10c7c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_26_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_26_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38ea2bd258563803f089ed79d897b2832d7b8e3440025bd60b6f3156ec0536ee -size 20690 +oid sha256:c003933d1fc60b69f01e71d0457fafc1808ab1b77473638be45145ad89150747 +size 20697 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_29_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_29_en.png index 02297f29ee..7d02982c4e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_29_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_29_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8cdac51735a02d6dd9f03a70937cf2a8c307bb8ef8636c6a136c389646cb2e67 -size 21150 +oid sha256:9ee35f103492b0b0fe4a2ad86f74f4d0283993ea63db039d034430bd4343e0bb +size 21160 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_2_en.png index 61a74b28d1..aababf615f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a38ee88772bb214f0b31c7338efc17e081dbe75b2123449305b6e62d406af1a1 -size 18359 +oid sha256:b030edd664a333f1ea2360ebcee1533c42f7abd3e96f54994afc760c5be4dc4e +size 18333 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_32_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_32_en.png index 4e6adf5cea..4152a5d16d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_32_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_32_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd5e60140ca1468932b81d2c708ce98066e1f4fcc0cd3a315986379ce1e66670 -size 21426 +oid sha256:191b2a9f3a8f449921f12acf72bfd58af0e85dec442dfcd81e04e342d5874e46 +size 21416 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_35_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_35_en.png index 1814093696..b40107e0ff 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_35_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_35_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32df3f3250f88aa77fea3278630d9f19abd45950211d520fd985969e933c7207 -size 16291 +oid sha256:f5de3ed181520441851573e3ddf3e72c8c952189a9c7b418e3e566114c6f02a7 +size 16270 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_38_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_38_en.png index 21606b47a9..d7014e2f20 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_38_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_38_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3672bb10f51f674dda4d06eced8f5a6e977bd10902d4de7bf9cc4e915d83bb7 -size 16836 +oid sha256:28003c233a356aa2409f60ed44daca0f4d3c1fb1a8689547d4e9f7f3a031163c +size 16812 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_41_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_41_en.png index 881a35582b..1c64a0f698 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_41_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_41_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e56fff6052b5c1b3b61a2aa73cad2f03881d86d465dbc59465d63c4aeac3ef4 -size 16371 +oid sha256:8b25313908ca35b61f8e79ed5e05cecd7ab69050de96c027bd4b0945dc68c5c9 +size 16374 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_44_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_44_en.png index 0565f21473..5e81d1fc12 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_44_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_44_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe30a0d96effe257973c893b6450a357d49f11e0f4743b2fdb16050fc15b3a8f -size 17549 +oid sha256:c2fcd26a3cb1486cf98d21df006334d997796d2e1329278bfb92097388cbe040 +size 17525 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_47_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_47_en.png index ee7f8a89cc..5fa7b054a7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_47_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_47_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96518c010196d80e8696ffcaea032468bb4d1edfbe6b1187286da488600363bf -size 17891 +oid sha256:c7ad48821a6fced07afc3169a100c483a04067a86100b5cef1a10bfdea7727a3 +size 17875 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_50_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_50_en.png index 83586a0c51..218c564337 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_50_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_50_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9dd0232c7847d5c0bb1047cfc6be5d4d43d2cc2e9347aff70a793103b511ad98 -size 21073 +oid sha256:19f08f3652a32b3b931c9ff1fa33f7d2c0b695279fc3acdbdb03767c98be57ac +size 21045 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_53_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_53_en.png index 4a3262b6ac..0c7a297bd3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_53_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_53_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1570611e0a894a88644956558629113717217ce4b7a76c615264bc2fa776616 -size 19763 +oid sha256:68cf5fb4245120f669a4b7a6cc4c306334fd61272e5b5ed927a6c6a414060296 +size 19803 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_56_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_56_en.png index b87579d730..8001a93c5c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_56_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_56_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68cb86e7966e17d3240de09ba69aa4af36e4b07c1b669b5003af8071d6eb1ee1 -size 13832 +oid sha256:496e055ab9bd30730cd8bf1b7f753a5f08017387b26afedd91339baedb19b40f +size 13831 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_59_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_59_en.png index 06eb0f06a4..86164b6cda 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_59_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_59_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e196a6ccf55855e8cad09e9934e09c8564cad08b4dcbc418c4a2b8d467088c3 -size 22891 +oid sha256:9cff65483f3ba03ec504b5c5a4d4e219dfb7e928819ed5172f85ee8a4c7a16a3 +size 22868 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_5_en.png index 98c1700236..36795d6b65 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5214f4d8366a00fca152708c65a265d1a56312d929439f41db5c3920f063f7ad -size 30095 +oid sha256:6989cce071211332a3b12d7a58aeab6d6c1c4c3b744fb8d4186e9e812ed96073 +size 30067 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_62_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_62_en.png index bc0c7cda4e..9f6a98b877 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_62_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_62_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:746b7181d7f9a3c25040624a2c20378b1eff1ce299840c6f4b4b9ca3f877bdfd -size 24976 +oid sha256:73d24101d6100c9c0a538cca0d5dd35fd83e6f7291c828963d23ca98378b7db3 +size 24950 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_65_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_65_en.png index dd81493a76..cc0be0fbbe 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_65_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_65_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67a8223f107fa05f1c5d66efc85f68325b4bf835e371ad44553bd6d6edd4a201 -size 18491 +oid sha256:c816d7ccf93dbf21f45508d7905450f10c6af01b13b923ec7568eeb1f5e2bb21 +size 18466 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_68_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_68_en.png index c1712835ff..30698aa052 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_68_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_68_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be8c830118af26d3d39a80d5dfa3a750e6e3830a30b066ca91b7ed305a2ee482 -size 23624 +oid sha256:855337a22ffae7779ec645572f28c13e453ec19bac975e51c390b97648882aa4 +size 23631 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_71_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_71_en.png index 0b8c15e54c..0ad9637d5f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_71_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_71_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:672f829a7227921b7179e91c1ee3ef58ff2b7dab7f131b687c4fe0b9862d2a2c -size 19436 +oid sha256:bca7ca1e5153f33b81542ecaa5800bb14bafcae4ee5398e64485f080078552c7 +size 19442 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_74_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_74_en.png index fc23c2c5ad..496b7bc706 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_74_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_74_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f3a7f0a4944f35fd726d5090eab08140b57332e6f7d8eb475fc6bf9ef37bcdf -size 26033 +oid sha256:0ead78f918f3123488ec2d59d09babd8443eb10b88c8d15f6e4aafc8817c0de3 +size 26063 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_77_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_77_en.png index b294ff8e75..904325ecd0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_77_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_77_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e88e6991bca9f0c99c28b6126b10135d0ba8de1faa7bc6174e6f66bc11b2ce03 -size 16794 +oid sha256:832084f232a204cd52913c85f7228422bca373feb63a36be419049591bb83a72 +size 16773 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_80_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_80_en.png index 1698660c8e..74cbc8dc97 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_80_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_80_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4979794c700bc8bab1bc767cc2387ce3be28b9d2c0b6da0696b237445ce7df95 -size 21225 +oid sha256:ce55aa9fd5c963d4e485bef493da6ac4b0cb434ef91f576fa1b4711617782dcc +size 21270 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_83_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_83_en.png index 72c687d439..045a15738a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_83_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_83_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:491cb82462df122b0e8d21c5b70e8db8ac19c28aaee1289db6f4774e7d31d53f -size 19556 +oid sha256:6102197c0eda927f879482f07abc2a6fafe1e688b57855c30e69bc7564ee8301 +size 19538 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_86_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_86_en.png index bd51d8c202..5d0a47c657 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_86_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_86_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bef7c3d043454a4faf858456c7fcc98d1a17a0846a891af3332e76c4e10b553 -size 17102 +oid sha256:ba3e873c2243c44c0b15393d26f7911f61965f4d1cc690f314df6c0b28739306 +size 17075 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_89_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_89_en.png index 62aa6aff31..70b32144d1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_89_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_89_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fd50a626209cc9b66c8e4847a0ba4e96e39ed2638235fc6e50b56b0d239961b -size 24405 +oid sha256:47fc297f230f9ff3d6bbb0ee295ca17443313836fc2910735288a94bbb41e923 +size 24442 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_8_en.png index 18efb9a84b..00472c5c5f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42ac664be2f0ed05b1fc844947556b256c823c5bc1bce380cceb1fb50ecd751f -size 25028 +oid sha256:2fa4309841b5093d542e4d5496f3209fa70d4cafad35d69cbb72d7dc1292b1b1 +size 25055 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_92_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_92_en.png index a4fd665ea1..b459e2de73 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_92_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.avatar_Avatar_Avatars_92_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42488ef04ffbbe625b7da963a2b1c3c9cabb0d80bb77a2d4f8b4c4afdcd9bf95 -size 20337 +oid sha256:90c5321decf7bf505f94312611bbf76d35725f758ff2f32ebfd321f7c172449e +size 20382 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Day_0_en.png index 084aff9841..cc57e03bb9 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77004b5d420cc5fe51f98b66033ed0d68cb9c308104fcdc281a0f06a6ece0fed -size 46964 +oid sha256:9d91be86dc5a346ccc9a9dcefbfd04fd229c5ae8367c8d94e6b99c7e4c48a34d +size 47007 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Night_0_en.png index 601c55c1b3..3c5397e73e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components_Bloom_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3467dc3bb6ed59048297d12badde6ab1748c19ff755809d4a14cf47e6c81ed9b -size 50973 +oid sha256:d409e87405152caf5d2f277a513aa3535d5aa22d5308fe21ee3749ecc77579b6 +size 50933 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png index 37b234801f..5555e6bb02 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerDark_DateTime_pickers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea4f3ed733159e0413383d8994943b5b7b3100160728138b7014a9567f665021 -size 30632 +oid sha256:3ecb9e2af8ad8221225e71463a1da8589ff1a6c9ba393f636c2dbec08fae513b +size 30991 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png index f957752384..897b8d763e 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components.previews_DatePickerLight_DateTime_pickers_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:657c14f3e42751aefbac5fc0b7d94462d2b90da1707ab7ce7997f5db1b29c6d2 -size 31013 +oid sha256:f7f456872a423c0edeb87e5a9fbc7e73896f3cb75609a64c4acb959ffd77e72d +size 31404 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_1_en.png index bd8972bb34..1a8c34f830 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fc5b7cad43115ba59798f8b0e31b0d5bc4b9efdedf8a2eacef7ad7d6eb156d6 -size 12926 +oid sha256:d27794acb3ee527509e2267fea260ad2d56b50b399c374a6070cee5c8c2d22de +size 12908 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_3_en.png index bd8972bb34..1a8c34f830 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fc5b7cad43115ba59798f8b0e31b0d5bc4b9efdedf8a2eacef7ad7d6eb156d6 -size 12926 +oid sha256:d27794acb3ee527509e2267fea260ad2d56b50b399c374a6070cee5c8c2d22de +size 12908 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_1_en.png index 70b39385e6..8aaafe7d13 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7cea6cba34ea64294fd4718fe79fac6e82c90cf552bfac3acc553b4e7684ddb -size 12860 +oid sha256:5a3feb1ae5105c3f716aeba67af6661df221119b458b9152704ce0b727aa448b +size 12864 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_3_en.png index 70b39385e6..8aaafe7d13 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_AttachmentThumbnail_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7cea6cba34ea64294fd4718fe79fac6e82c90cf552bfac3acc553b4e7684ddb -size 12860 +oid sha256:5a3feb1ae5105c3f716aeba67af6661df221119b458b9152704ce0b727aa448b +size 12864 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_1_en.png index 9de6354dcb..e528eae056 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4aa6994d79512b53deb30000d354401bbb0e5c7b716ff9ce78edc0a5ec1a700 -size 8663 +oid sha256:a7e27785d6c6c8fee1ca9e0a5a3d68373ef73cb7c8cabc811a4dd1c0dec27105 +size 8615 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_2_en.png index b4b7bf5bed..e528eae056 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3744f13869dbb38959f4dd0887dec02c0a94d9d080e1f89d3891c9ff9d61b20 -size 32753 +oid sha256:a7e27785d6c6c8fee1ca9e0a5a3d68373ef73cb7c8cabc811a4dd1c0dec27105 +size 8615 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_1_en.png index 49e829c8b7..b9bf089cc7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3155928c36bc44d440cfa4abc88dcd50247cac0ae8769c34a3bcfb407da77a0d -size 8906 +oid sha256:95e047cfbfd7ec5f585a80bf77b9456679715a5f18dbec54ce44940767716b00 +size 8859 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_2_en.png index 4eeb76bd3e..b9bf089cc7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.components_EditableAvatarView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25e8d307a6be5516f3cf7311dec41fb131a2be3e4b66c54d5024cb3a9f0b5d53 -size 32465 +oid sha256:95e047cfbfd7ec5f585a80bf77b9456679715a5f18dbec54ce44940767716b00 +size 8859 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_5_en.png index b1e7386915..f101b8a3b0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:444e50fa6d7667b9295aaccbc059318e1564a3e4f7d83a360ca28bf750a4ed1b -size 6157 +oid sha256:c64d85a120267324f9f236eb177786eaa04f6a8632abbd8eb4069a9c24cfcc97 +size 17751 diff --git a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_5_en.png index 4c1860381a..cc91d9f3aa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.matrix.ui.messages.reply_InReplyToView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb8ea0027d6e4eb841d570c008468eaaacb2c841cfb7fa70d84c1f9e50dd9f36 -size 6147 +oid sha256:9d3261765aae0bf2aa049267678e51192286dfe830747c202431d043189a1307 +size 17512 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en.png index a027c89303..42704f5647 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15d3fa6a95cda6bca06ad79d3f4862db05e38111cdcac47c1cdd3aa204bc1f97 -size 4210 +oid sha256:871cede89b11408db3873b7512f204a30c10461e5182ed1dfc0ccbf459e5ae86 +size 509431 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en.png index 503f2bb229..bdf2a68f05 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_ImageItemView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abaae9e0c6bf9d7dec701e9a51592e89408668e0a2b8325731efdfdc73978acd -size 3667 +oid sha256:c2f098a79f6aa516c3624481c573b730de1d978616068ca2e9994850922a1172 +size 508910 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en.png index f5cb7d82cd..dd1be5c2ba 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00159ed8d968d53970e4a3b7f82ab542fb5eabfc4513dd68d0eef05f0615373e -size 7290 +oid sha256:0d4f9eb0aa4caf7300e1d46c012caa420d75359b31d4312c3c074153476f8589 +size 508761 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en.png index 6665c107c1..ace5077434 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:efb12b63fde67256b255503d00848d64480e142c54619329b75aeed451a3dd17 -size 6375 +oid sha256:af4c41c3e23c6669f42ee6413e9d7dee7b166fd4ccd8d189eb4df96601fb7554 +size 508550 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en.png index d58f4c3155..82127ffa23 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f6835bd79d18d202c1d21b00a1afa4fa2c7cbfaf8f586a1dd1f48afdd5f69e5 -size 7644 +oid sha256:c3e88b94e81ee3ae2cf7c091e6fbe9b7668b88fc675ec41342b1c27d817013cf +size 509402 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en.png index dc363be75b..1dc78b8c9c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery.ui_VideoItemView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:452afc2e04191eb82de772597ee97987eda5667ff56ecb684bb3b9e0bef90435 -size 6737 +oid sha256:1e9b94a224bd3b8ac9b3722a5817b315497ea48c1847b3e5fc796482070c5844 +size 508924 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png index 9807f8bd54..0581a5f651 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ee8d9e829efc0723d62411e2069a8dac90b18069d1d8cd5dc5ec6a5b9899a14 -size 21572 +oid sha256:b5f93cf8983f5e2a34fd777ae386f09aa5a54dddc29f5d3eda3d5ccb71f267ff +size 528450 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png index 5c067f246f..788b2a0eb3 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcb5bc041286ba863ae982b2ad03873a76e48ed6ebd5d35c82dea269d86363a7 -size 21189 +oid sha256:8d4e5c1cc6d8e8d5d7b7b5d25ed70efb840f9ca18336d57d0a471753e9674dba +size 524899 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_13_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_13_en.png index dca6c37dac..c1734088d7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_13_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_13_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ff3f1976405db6abc9b7a1bb0119006bcf1b464c88b3f9e09188528e9e5608e -size 24491 +oid sha256:28006561c0ff11c97ed9e16a090a008700066f3740eec35b2a418c4ed2cdcab1 +size 133897 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_16_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_16_en.png index 504244640a..f701e5bac8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_16_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_16_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dec2e6f9dcfc2e92fef730a26599d6a4e2e09b6b9999dc912a82917f65908417 -size 6798 +oid sha256:e2a48623119e17e47184b6925f9fcb4c5ed9625765e2e6a8537041ec9253ed4d +size 113570 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png index 1d94cf34b4..9cf4813cc0 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:759b0ce50cfebd8d4b06cf8d61eacfd5c2603d0916e0ec399afb35b3d06e9d36 -size 25074 +oid sha256:4ca520b7761afc1a88c83e947f0bd1c8a19b65430c78735bace8038d4680077f +size 131596 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_5_en.png index c91d06c0f3..487ecf30fa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74a1a871d7bee0818b8154f8d7dba9e30c776d97137d7cbe3e8d445adbd726ad -size 5409 +oid sha256:34e2486d2d6eb25df20978ca2a9f83ed7dc35aae7a506f9077ece1aac292f799 +size 112101 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_6_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_6_en.png index 81c7520fe2..2d79de708b 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_6_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:605740d70c27cf6575f81fc5a68218b80c7207c4a90704ec5d7534ddaf4df0ad -size 14181 +oid sha256:ccefa0617f544b53e8d150bcadae23b8050638a640355c10dac6ffe081a94d05 +size 123336 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png index cf576c41d8..43c4783b62 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee1b8cbedf1d52a2c76d497ac100bb578f6d86ed33b4bb5f4b60f35cc5f208d2 -size 26437 +oid sha256:b6f583b3861c9d20508c886db1dfc9f7fc981673b7b7c5737f075ebb5e4f0dc9 +size 136609 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_9_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_9_en.png index 0abb098fec..d32210f468 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_9_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d61c3c521e0a380ee9ac33e4bebdfc98e91b23a47dd9f7c06bcbb299279ae2d -size 26487 +oid sha256:909837b6fc20a48624952f7ef99ef0638a507cef08d5dab8285aa527f0b3d1a1 +size 136717 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png deleted file mode 100644 index 785f56af7f..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:154a56d848cc735efc25274052ce92b1a20bc8f80f75e91d0d4cfc7d488cd246 -size 5874 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png deleted file mode 100644 index 3f13dad18f..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Day_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9ff42261f4d991650578ceadaa13be0b1924c1af8f71cf10832f53c7d958827d -size 9317 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png deleted file mode 100644 index c41c1c7921..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2dd3462935091cfe022f0e6fc71b082eed7dab4769def13398a81a90f871b61a -size 5807 diff --git a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png deleted file mode 100644 index 17ecfab090..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.oidc.impl.webview_OidcView_Night_1_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e6158b2d0447199de84156fbfdc7bd9aa1af7ff2092b498db254ded465605b76 -size 8208 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png index 487d65df61..108c2af37a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ba9b65e68427ae8f00f3075eeb671d2980c29ce904e10398bc26e3455bc641f -size 60114 +oid sha256:bd3639f1d57e2a9e313a6626625c600557cc90e90326310441366e0177f17ada +size 90429 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png index f6f5f00173..5429072dca 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReplyNotEncrypted_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3a6b63e7c35d6f64cb5cc59af99c558b0ac5ed5ce9f1de1fb78c819f156c880 -size 58366 +oid sha256:55f9442c5fba907d6602b4f99700c28e5bb2b0229b599bc0497b9e7fd01f4adc +size 87380 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png index 637d7eb415..093e104a2d 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9d24dc3f0540f23d6b5511844b5b06511a757941dbf36277faff0fc5b376efd -size 60366 +oid sha256:a3b31d74b92b2db69c548a78606b606751644124703f5163482b4fe0e0471b6e +size 104792 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png index 41ce25f743..9f856565c8 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerReply_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf7666582d6bf855ef40930ae5410ba90eb8808b135c82745ad9ffd354a1c239 -size 58469 +oid sha256:7b2b966f1a3b55058d3173df9c80a7492be710f2310fab0d0ae3950e820526a7 +size 101725 diff --git a/tools/dependencies/checkDependencies.py b/tools/dependencies/checkDependencies.py index 3067e32261..29ef7ea7fc 100755 --- a/tools/dependencies/checkDependencies.py +++ b/tools/dependencies/checkDependencies.py @@ -56,7 +56,7 @@ def checkThatThereIsNoTestDependency(dependencies): continue else: subProject = line.split(" ")[-1] - if subProject.endswith(":test") or ":tests:" in subProject: + if subProject.endswith(":test") or ":tests:" in subProject and "detekt-rules" not in subProject: error = "Error: '" + currentProject + "' depends on the test project '" + subProject + "'\n" error += " Please replace occurrence(s) of 'implementation(projects" + subProject.replace(":", ".") + ")'" error += " with 'testImplementation(projects" + subProject.replace(":", ".") + ")'." diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index 71187feced..9af20180c6 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -230,6 +230,7 @@ Compose: - LocalMentionSpanUpdater - LocalAnalyticsService - LocalBuildMeta + - LocalUiTestMode CompositionLocalNaming: active: true ContentEmitterReturningValues: