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 7e782c7cc4..38dc39ee83 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -39,7 +39,6 @@ import com.bumble.appyx.navmodel.backstack.operation.replace import com.bumble.appyx.navmodel.backstack.operation.singleTop import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject -import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.annotations.ContributesNode import io.element.android.appnav.loggedin.LoggedInNode import io.element.android.appnav.loggedin.MediaPreviewConfigMigration @@ -84,6 +83,7 @@ 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.permalink.PermalinkData +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener import io.element.android.libraries.matrix.api.verification.VerificationRequest @@ -109,6 +109,7 @@ import java.util.UUID import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlin.time.toKotlinDuration +import im.vector.app.features.analytics.plan.JoinedRoom as JoinedRoomAnalyticsEvent @ContributesNode(SessionScope::class) @AssistedInject @@ -265,7 +266,7 @@ class LoggedInFlowNode( data class Room( val roomIdOrAlias: RoomIdOrAlias, val serverNames: List = emptyList(), - val trigger: JoinedRoom.Trigger? = null, + val trigger: JoinedRoomAnalyticsEvent.Trigger? = null, val roomDescription: RoomDescription? = null, val initialElement: RoomNavigationTarget = RoomNavigationTarget.Root(), val targetId: UUID = UUID.randomUUID(), @@ -315,8 +316,13 @@ class LoggedInFlowNode( } NavTarget.Home -> { val callback = object : HomeEntryPoint.Callback { - override fun navigateToRoom(roomId: RoomId) { - backstack.push(NavTarget.Room(roomId.toRoomIdOrAlias())) + override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) { + backstack.push( + NavTarget.Room( + roomIdOrAlias = roomId.toRoomIdOrAlias(), + initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom) + ) + ) } override fun navigateToSettings() { @@ -365,7 +371,7 @@ class LoggedInFlowNode( val target = NavTarget.Room( roomIdOrAlias = data.roomIdOrAlias, serverNames = data.viaParameters, - trigger = JoinedRoom.Trigger.Timeline, + trigger = JoinedRoomAnalyticsEvent.Trigger.Timeline, initialElement = RoomNavigationTarget.Root(data.eventId), ) if (pushToBackstack) { @@ -479,7 +485,7 @@ class LoggedInFlowNode( NavTarget.Room( roomIdOrAlias = roomDescription.roomId.toRoomIdOrAlias(), roomDescription = roomDescription, - trigger = JoinedRoom.Trigger.RoomDirectory, + trigger = JoinedRoomAnalyticsEvent.Trigger.RoomDirectory, ) ) } @@ -519,7 +525,7 @@ class LoggedInFlowNode( suspend fun attachRoom( roomIdOrAlias: RoomIdOrAlias, serverNames: List = emptyList(), - trigger: JoinedRoom.Trigger? = null, + trigger: JoinedRoomAnalyticsEvent.Trigger? = null, eventId: EventId? = null, clearBackstack: Boolean, ): RoomFlowNode { 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 9aa62d6b16..1bf3516900 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 @@ -19,10 +19,10 @@ import com.bumble.appyx.core.node.node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.active import com.bumble.appyx.navmodel.backstack.operation.newRoot import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedInject -import im.vector.app.features.analytics.plan.JoinedRoom import io.element.android.annotations.ContributesNode import io.element.android.appnav.room.joined.JoinedRoomFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode @@ -60,10 +60,13 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import timber.log.Timber import java.util.Optional import kotlin.jvm.optionals.getOrNull +import im.vector.app.features.analytics.plan.JoinedRoom as JoinedRoomAnalyticsEvent +import io.element.android.libraries.matrix.api.room.JoinedRoom as JoinedRoomInstance @ContributesNode(SessionScope::class) @AssistedInject @@ -77,7 +80,14 @@ class RoomFlowNode( private val analyticsService: AnalyticsService, ) : BaseFlowNode( backstack = BackStack( - initialElement = NavTarget.Loading, + initialElement = run { + val joinedRoom = (plugins.filterIsInstance().first().initialElement as? RoomNavigationTarget.Root)?.joinedRoom + if (joinedRoom != null) { + NavTarget.JoinedRoom(joinedRoom) + } else { + NavTarget.Loading + } + }, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -87,7 +97,7 @@ class RoomFlowNode( val roomIdOrAlias: RoomIdOrAlias, val roomDescription: Optional, val serverNames: List, - val trigger: Optional, + val trigger: Optional, val initialElement: RoomNavigationTarget, ) : NodeInputs @@ -104,11 +114,16 @@ class RoomFlowNode( data class JoinRoom( val roomId: RoomId, val serverNames: List, - val trigger: im.vector.app.features.analytics.plan.JoinedRoom.Trigger, + val trigger: JoinedRoomAnalyticsEvent.Trigger, ) : NavTarget @Parcelize - data class JoinedRoom(val roomId: RoomId) : NavTarget + data class JoinedRoom( + val roomId: RoomId, + @IgnoredOnParcel val joinedRoom: JoinedRoomInstance? = null, + ) : NavTarget { + constructor(joinedRoom: JoinedRoomInstance) : this(joinedRoom.roomId, joinedRoom) + } } override fun onBuilt() { @@ -133,7 +148,9 @@ class RoomFlowNode( } private fun subscribeToRoomInfoFlow(roomId: RoomId, serverNames: List) { - val roomInfoFlow = client.getRoomInfoFlow(roomId) + val joinedRoom = (inputs.initialElement as? RoomNavigationTarget.Root)?.joinedRoom + val roomInfoFlow = joinedRoom?.roomInfoFlow?.map { Optional.of(it) } + ?: client.getRoomInfoFlow(roomId) // This observes the local membership changes for the room val membershipUpdateFlow = membershipObserver.updates @@ -149,6 +166,11 @@ class RoomFlowNode( currentMembershipFlow.onEach { (previousMembership, membership) -> Timber.d("Room membership: $membership") if (membership == CurrentUserMembership.JOINED) { + val currentNavTarget = backstack.active?.key?.navTarget + if (currentNavTarget is NavTarget.JoinedRoom && currentNavTarget.roomId == roomId) { + Timber.d("Already in JoinedRoom $roomId, do nothing") + return@onEach + } backstack.newRoot(NavTarget.JoinedRoom(roomId)) } else { val leavingFromCurrentDevice = @@ -163,7 +185,7 @@ class RoomFlowNode( NavTarget.JoinRoom( roomId = roomId, serverNames = serverNames, - trigger = inputs.trigger.getOrNull() ?: JoinedRoom.Trigger.Invite, + trigger = inputs.trigger.getOrNull() ?: JoinedRoomAnalyticsEvent.Trigger.Invite, ) ) } @@ -209,7 +231,8 @@ class RoomFlowNode( val roomFlowNodeCallback = plugins() val inputs = JoinedRoomFlowNode.Inputs( roomId = navTarget.roomId, - initialElement = inputs.initialElement + initialElement = inputs.initialElement, + joinedRoom = navTarget.joinedRoom, ) createNode(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt index 93403371c9..aac916ab9d 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomNavigationTarget.kt @@ -10,12 +10,15 @@ package io.element.android.appnav.room import android.os.Parcelable import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.room.JoinedRoom +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize sealed interface RoomNavigationTarget : Parcelable { @Parcelize data class Root( val eventId: EventId? = null, + @IgnoredOnParcel val joinedRoom: JoinedRoom? = null, ) : RoomNavigationTarget @Parcelize diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt index 85710deffe..504bdfec58 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/joined/JoinedRoomFlowNode.kt @@ -38,6 +38,7 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.ThreadId +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.ui.room.LoadingRoomState import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory import kotlinx.coroutines.flow.distinctUntilChanged @@ -63,11 +64,12 @@ class JoinedRoomFlowNode( ) { data class Inputs( val roomId: RoomId, + val joinedRoom: JoinedRoom?, val initialElement: RoomNavigationTarget, ) : NodeInputs private val inputs: Inputs = inputs() - private val loadingRoomStateStateFlow = loadingRoomStateFlowFactory.create(lifecycleScope, inputs.roomId) + private val loadingRoomStateStateFlow = loadingRoomStateFlowFactory.create(lifecycleScope, inputs.roomId, inputs.joinedRoom) sealed interface NavTarget : Parcelable { @Parcelize diff --git a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt index c8810dff92..14128ac33a 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/room/LoadingBaseRoomStateFlowFactoryTest.kt @@ -23,6 +23,19 @@ import kotlinx.coroutines.test.runTest import org.junit.Test class LoadingBaseRoomStateFlowFactoryTest { + @Test + fun `flow should emit only Loaded when we already pass a JoinedRoom`() = runTest { + val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID)) + val matrixClient = FakeMatrixClient(A_SESSION_ID) + val flowFactory = LoadingRoomStateFlowFactory(matrixClient) + flowFactory + .create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = room) + .test { + assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room)) + ensureAllEventsConsumed() + } + } + @Test fun `flow should emit Loading and then Loaded when there is a room in cache`() = runTest { val room = FakeJoinedRoom(baseRoom = FakeBaseRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID)) @@ -31,7 +44,7 @@ class LoadingBaseRoomStateFlowFactoryTest { } val flowFactory = LoadingRoomStateFlowFactory(matrixClient) flowFactory - .create(this, A_ROOM_ID) + .create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null) .test { assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room)) @@ -45,7 +58,7 @@ class LoadingBaseRoomStateFlowFactoryTest { val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService) val flowFactory = LoadingRoomStateFlowFactory(matrixClient) flowFactory - .create(this, A_ROOM_ID) + .create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null) .test { assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) matrixClient.givenGetRoomResult(A_ROOM_ID, room) @@ -60,7 +73,7 @@ class LoadingBaseRoomStateFlowFactoryTest { val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService) val flowFactory = LoadingRoomStateFlowFactory(matrixClient) flowFactory - .create(this, A_ROOM_ID) + .create(lifecycleScope = this, roomId = A_ROOM_ID, joinedRoom = null) .test { assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1)) diff --git a/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt b/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt index b3dd8f1ee1..71ee093985 100644 --- a/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt +++ b/features/home/api/src/main/kotlin/io/element/android/features/home/api/HomeEntryPoint.kt @@ -13,6 +13,7 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.JoinedRoom interface HomeEntryPoint : FeatureEntryPoint { fun createNode( @@ -22,7 +23,7 @@ interface HomeEntryPoint : FeatureEntryPoint { ): Node interface Callback : Plugin { - fun navigateToRoom(roomId: RoomId) + fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) fun navigateToCreateRoom() fun navigateToSettings() fun navigateToSetUpRecovery() diff --git a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt index 78e48fad79..d9f87e2edd 100644 --- a/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt +++ b/features/home/impl/src/main/kotlin/io/element/android/features/home/impl/HomeFlowNode.kt @@ -14,6 +14,8 @@ import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope @@ -41,20 +43,33 @@ import io.element.android.features.logout.api.direct.DirectLogoutView import io.element.android.features.reportroom.api.ReportRoomEntryPoint import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesEntryPoint import io.element.android.features.rolesandpermissions.api.ChangeRoomMemberRolesListType +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.appyx.launchMolecule import io.element.android.libraries.architecture.callback +import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.deeplink.api.usecase.InviteFriendsUseCase +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.utils.DelayedVisibility import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.job import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize +import timber.log.Timber +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.milliseconds @ContributesNode(SessionScope::class) @AssistedInject @@ -71,6 +86,7 @@ class HomeFlowNode( private val declineInviteAndBlockUserEntryPoint: DeclineInviteAndBlockEntryPoint, private val changeRoomMemberRolesEntryPoint: ChangeRoomMemberRolesEntryPoint, private val leaveRoomRenderer: LeaveRoomRenderer, + @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Root, @@ -150,9 +166,58 @@ class HomeFlowNode( return node(buildContext) { modifier -> val state by stateFlow.collectAsState() val activity = requireNotNull(LocalActivity.current) + + val loadingJoinedRoomJob = remember { mutableStateOf>(AsyncData.Uninitialized) } + if (loadingJoinedRoomJob.value.isLoading()) { + DelayedVisibility(duration = 400.milliseconds) { + ProgressDialog( + onDismissRequest = { + loadingJoinedRoomJob.value.dataOrNull()?.cancel() + loadingJoinedRoomJob.value = AsyncData.Uninitialized + } + ) + } + } + + fun navigateToRoom( + roomId: RoomId, + ) { + if (!loadingJoinedRoomJob.value.isUninitialized()) { + Timber.w("Already loading a room, ignoring navigateToRoom for $roomId") + return + } + + val job = sessionCoroutineScope.launch { + runCatchingExceptions { + matrixClient.getJoinedRoom(roomId) + }.fold( + onSuccess = { joinedRoom -> + if (isActive) { + callback.navigateToRoom(roomId, joinedRoom) + loadingJoinedRoomJob.value = AsyncData.Success(coroutineContext.job) + // Wait a bit before resetting the state to avoid allowing to open several rooms + delay(200.milliseconds) + loadingJoinedRoomJob.value = AsyncData.Uninitialized + } + }, + onFailure = { + // If the operation wasn't cancelled, navigate without the room, using the room id + if (it !is CancellationException) { + callback.navigateToRoom(roomId, null) + } + loadingJoinedRoomJob.value = AsyncData.Failure(error = it, prevData = coroutineContext.job) + // Wait a bit before resetting the state to avoid allowing to open several rooms + delay(200.milliseconds) + loadingJoinedRoomJob.value = AsyncData.Uninitialized + } + ) + } + loadingJoinedRoomJob.value = AsyncData.Loading(job) + } + HomeView( homeState = state, - onRoomClick = callback::navigateToRoom, + onRoomClick = ::navigateToRoom, onSettingsClick = callback::navigateToSettings, onStartChatClick = callback::navigateToCreateRoom, onSetUpRecoveryClick = callback::navigateToSetUpRecovery, @@ -165,7 +230,7 @@ class HomeFlowNode( acceptDeclineInviteView = { acceptDeclineInviteView.Render( state = state.roomListState.acceptDeclineInviteState, - onAcceptInviteSuccess = callback::navigateToRoom, + onAcceptInviteSuccess = ::navigateToRoom, onDeclineInviteSuccess = { }, modifier = Modifier ) diff --git a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt index ed6482b0e7..9778556dd2 100644 --- a/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt +++ b/features/home/impl/src/test/kotlin/io/element/android/features/home/impl/DefaultHomeEntryPointTest.kt @@ -13,17 +13,19 @@ import com.bumble.appyx.core.modality.BuildContext import com.google.common.truth.Truth.assertThat import io.element.android.features.home.api.HomeEntryPoint import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.node.TestParentNode +import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class DefaultHomeEntryPointTest { @Test - fun `test node builder`() { + fun `test node builder`() = runTest { val entryPoint = DefaultHomeEntryPoint() val parentNode = TestParentNode.create { buildContext, plugins -> HomeFlowNode( @@ -39,10 +41,11 @@ class DefaultHomeEntryPointTest { declineInviteAndBlockUserEntryPoint = { _, _, _ -> lambdaError() }, changeRoomMemberRolesEntryPoint = { _, _, _, _ -> lambdaError() }, leaveRoomRenderer = { _, _, _ -> lambdaError() }, + sessionCoroutineScope = backgroundScope, ) } val callback = object : HomeEntryPoint.Callback { - override fun navigateToRoom(roomId: RoomId) = lambdaError() + override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) = lambdaError() override fun navigateToCreateRoom() = lambdaError() override fun navigateToSettings() = lambdaError() override fun navigateToSetUpRecovery() = lambdaError() 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 7b8ff837c1..e44cac4f27 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 @@ -65,6 +65,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -261,6 +262,7 @@ class TimelinePresenter( items } .onEach(redactedVoiceMessageManager::onEachMatrixTimelineItem) + .flowOn(dispatchers.computation) .launchIn(this) } 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 5ff8e5d999..55498c150d 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 @@ -36,6 +36,7 @@ import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.DateDividerMode import org.matrix.rustcomponents.sdk.Membership import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.TimelineConfiguration import org.matrix.rustcomponents.sdk.TimelineFilter import org.matrix.rustcomponents.sdk.TimelineFocus @@ -60,7 +61,7 @@ class RustRoomFactory( private val roomInfoMapper: RoomInfoMapper, private val analyticsService: AnalyticsService, ) { - private val dispatcher = dispatchers.io.limitedParallelism(1) + private val dispatcher = dispatchers.computation.limitedParallelism(1) private val mutex = Mutex() private val isDestroyed: AtomicBoolean = AtomicBoolean(false) @@ -86,24 +87,21 @@ class RustRoomFactory( return@withContext null } val room = awaitRoomInRoomList(roomId) ?: return@withContext null - getBaseRoom(room) + getBaseRoom(sdkRoom = room, roomInfo = room.roomInfo()) } } - private suspend fun getBaseRoom(sdkRoom: Room): RustBaseRoom { - val initialRoomInfo = sdkRoom.roomInfo() - return RustBaseRoom( - sessionId = sessionId, - deviceId = deviceId, - innerRoom = sdkRoom, - coroutineDispatchers = dispatchers, - roomSyncSubscriber = roomSyncSubscriber, - roomMembershipObserver = roomMembershipObserver, - roomInfoMapper = roomInfoMapper, - initialRoomInfo = roomInfoMapper.map(initialRoomInfo), - sessionCoroutineScope = sessionCoroutineScope, - ) - } + private fun getBaseRoom(sdkRoom: Room, roomInfo: RoomInfo) = RustBaseRoom( + sessionId = sessionId, + deviceId = deviceId, + innerRoom = sdkRoom, + coroutineDispatchers = dispatchers, + roomSyncSubscriber = roomSyncSubscriber, + roomMembershipObserver = roomMembershipObserver, + roomInfoMapper = roomInfoMapper, + initialRoomInfo = roomInfoMapper.map(roomInfo), + sessionCoroutineScope = sessionCoroutineScope, + ) suspend fun getJoinedRoomOrPreview(roomId: RoomId, serverNames: List): GetRoomResult? = withContext(dispatcher) { mutex.withLock { @@ -113,10 +111,11 @@ class RustRoomFactory( } val sdkRoom = awaitRoomInRoomList(roomId) ?: return@withLock null + val roomInfo = sdkRoom.roomInfo() val parentTransaction = analyticsService.getLongRunningTransaction(AnalyticsLongRunningTransaction.OpenRoom) - if (sdkRoom.membership() == Membership.JOINED) { + if (roomInfo.membership == Membership.JOINED) { analyticsService.recordTransaction( name = "Get joined room", operation = "RustRoomFactory.getJoinedRoomOrPreview", @@ -140,10 +139,9 @@ class RustRoomFactory( ) } - val baseRoom = transaction.recordChildTransaction(operation = "getBaseRoom", description = "Get room from SDK") { getBaseRoom(sdkRoom) } GetRoomResult.Joined( JoinedRustRoom( - baseRoom = baseRoom, + baseRoom = getBaseRoom(sdkRoom, roomInfo), notificationSettingsService = notificationSettingsService, roomContentForwarder = roomContentForwarder, liveInnerTimeline = timeline, @@ -169,7 +167,7 @@ class RustRoomFactory( GetRoomResult.NotJoined( NotJoinedRustRoom( sessionId = sessionId, - localRoom = getBaseRoom(sdkRoom), + localRoom = getBaseRoom(sdkRoom, roomInfo), previewInfo = RoomPreviewInfoMapper.map(preview.info()), ) ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt index 0b7c16b3a0..cbbe79b480 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/LoadingRoomState.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.JoinedRoom import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asFlow @@ -39,16 +40,21 @@ open class LoadingRoomStateProvider : PreviewParameterProvider @Inject class LoadingRoomStateFlowFactory(private val matrixClient: MatrixClient) { - fun create(lifecycleScope: CoroutineScope, roomId: RoomId): StateFlow = - getJoinedRoomFlow(roomId) - .map { room -> - if (room != null) { - LoadingRoomState.Loaded(room) - } else { - LoadingRoomState.Error + fun create(lifecycleScope: CoroutineScope, roomId: RoomId, joinedRoom: JoinedRoom?): StateFlow { + return if (joinedRoom != null) { + MutableStateFlow(LoadingRoomState.Loaded(joinedRoom)) + } else { + getJoinedRoomFlow(roomId) + .map { room -> + if (room != null) { + LoadingRoomState.Loaded(room) + } else { + LoadingRoomState.Error + } } - } - .stateIn(lifecycleScope, SharingStarted.Eagerly, LoadingRoomState.Loading) + .stateIn(lifecycleScope, SharingStarted.Eagerly, LoadingRoomState.Loading) + } + } private fun getJoinedRoomFlow(roomId: RoomId): Flow = suspend { matrixClient.getJoinedRoom(roomId = roomId) diff --git a/tests/uitests/build.gradle.kts b/tests/uitests/build.gradle.kts index ea43284476..53876b0c8c 100644 --- a/tests/uitests/build.gradle.kts +++ b/tests/uitests/build.gradle.kts @@ -20,6 +20,11 @@ android { namespace = "ui" } +tasks.withType(Test::class.java) { + // Don't fail the test run if there are no tests, this can happen if we run them with screenshot test disabled + failOnNoDiscoveredTests = false +} + dependencies { // Paparazzi 1.3.2 workaround (see https://github.com/cashapp/paparazzi/blob/master/CHANGELOG.md#132---2024-01-13) constraints.add("testImplementation", "com.google.guava:guava") {