diff --git a/appnav/src/main/kotlin/io/element/android/appnav/AwaitRoomNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/AwaitRoomNode.kt new file mode 100644 index 0000000000..6b8962e30c --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/AwaitRoomNode.kt @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.appnav + +import android.os.Parcelable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import com.bumble.appyx.navmodel.backstack.BackStack +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.architecture.BackstackNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +class AwaitRoomNode @AssistedInject constructor( + @Assisted val buildContext: BuildContext, + @Assisted plugins: List, + private val matrixClient: MatrixClient, +) : + BackstackNode( + backstack = BackStack( + initialElement = NavTarget.Loading, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins + ) { + + data class Inputs( + val roomId: RoomId, + val initialElement: RoomFlowNode.NavTarget = RoomFlowNode.NavTarget.Messages, + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val roomStateFlow = suspend { + matrixClient.getRoom(roomId = inputs.roomId) + } + .asFlow() + .stateIn(lifecycleScope, SharingStarted.Eagerly, null) + + sealed interface NavTarget : Parcelable { + @Parcelize + object Loading : NavTarget + + @Parcelize + object Loaded : NavTarget + } + + init { + roomStateFlow.onEach { room -> + if (room == null) { + backstack.safeRoot(NavTarget.Loading) + } else { + backstack.safeRoot(NavTarget.Loaded) + } + }.launchIn(lifecycleScope) + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Loaded -> { + val nodeLifecycleCallbacks = plugins() + val roomFlowNodeCallback = plugins() + val room = roomStateFlow.value + if (room == null) { + loadingNode(buildContext) + } else { + val inputs = RoomFlowNode.Inputs(room, initialElement = inputs.initialElement) + createNode(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback + nodeLifecycleCallbacks) + } + } + NavTarget.Loading -> { + loadingNode(buildContext) + } + } + } + + private fun loadingNode(buildContext: BuildContext) = node(buildContext) { + Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + + @Composable + override fun View(modifier: Modifier) { + Children( + navModel = backstack, + modifier = modifier, + ) + } +} + 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 4d6553f51c..ba3d228566 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -57,7 +57,6 @@ import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient @@ -147,6 +146,7 @@ class LoggedInFlowNode @AssistedInject constructor( observeAnalyticsState() lifecycle.subscribe( onCreate = { + syncService.startSync() plugins().forEach { it.onFlowCreated(id, inputs.matrixClient) } val imageLoaderFactory = bindings().loggedInImageLoaderFactory() Coil.setImageLoader(imageLoaderFactory) @@ -267,24 +267,14 @@ class LoggedInFlowNode @AssistedInject constructor( .build() } is NavTarget.Room -> { - val room = inputs.matrixClient.getRoom(roomId = navTarget.roomId) - if (room == null) { - // TODO CREATE UNKNOWN ROOM NODE - node(buildContext) { - Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(text = "Unknown room with id = ${navTarget.roomId}") - } - } - } else { val nodeLifecycleCallbacks = plugins() val callback = object : RoomFlowNode.Callback { override fun onForwardedToSingleRoom(roomId: RoomId) { coroutineScope.launch { attachRoom(roomId) } } } - val inputs = RoomFlowNode.Inputs(room, initialElement = navTarget.initialElement) - createNode(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks) - } + val inputs = AwaitRoomNode.Inputs(roomId = navTarget.roomId, initialElement = navTarget.initialElement) + createNode(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks) } NavTarget.Settings -> { val callback = object : PreferencesEntryPoint.Callback { @@ -342,7 +332,7 @@ class LoggedInFlowNode @AssistedInject constructor( } } - suspend fun attachRoom(roomId: RoomId): RoomFlowNode { + suspend fun attachRoom(roomId: RoomId): AwaitRoomNode { return attachChild { backstack.singleTop(NavTarget.RoomList) backstack.push(NavTarget.Room(roomId)) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 67bf22d22a..20d3309a5f 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -65,20 +65,9 @@ class CreateRoomRootPresenter @Inject constructor( val localCoroutineScope = rememberCoroutineScope() val startDmAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } - fun startDm(matrixUser: MatrixUser) { - startDmAction.value = Async.Uninitialized - matrixClient.findDM(matrixUser.userId).use { existingDM -> - if (existingDM == null) { - localCoroutineScope.createDM(matrixUser, startDmAction) - } else { - startDmAction.value = Async.Success(existingDM.roomId) - } - } - } - fun handleEvents(event: CreateRoomRootEvents) { when (event) { - is CreateRoomRootEvents.StartDM -> startDm(event.matrixUser) + is CreateRoomRootEvents.StartDM -> localCoroutineScope.startDm(event.matrixUser, startDmAction) CreateRoomRootEvents.CancelStartDM -> startDmAction.value = Async.Uninitialized } } @@ -91,10 +80,20 @@ class CreateRoomRootPresenter @Inject constructor( ) } - private fun CoroutineScope.createDM(user: MatrixUser, startDmAction: MutableState>) = launch { + private fun CoroutineScope.startDm(matrixUser: MatrixUser, startDmAction: MutableState>) = launch { suspend { - matrixClient.createDM(user.userId).getOrThrow() - .also { analyticsService.capture(CreatedRoom(isDM = true)) } + matrixClient.findDM(matrixUser.userId).use { existingDM -> + existingDM?.roomId ?: createDM(matrixUser) + } }.runCatchingUpdatingState(startDmAction) } + + private suspend fun createDM(user: MatrixUser): RoomId { + return matrixClient + .createDM(user.userId) + .onSuccess { + analyticsService.capture(CreatedRoom(isDM = true)) + } + .getOrThrow() + } } diff --git a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImpl.kt b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImpl.kt index 4978a87815..68a8d80fe6 100644 --- a/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImpl.kt +++ b/features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImpl.kt @@ -78,7 +78,7 @@ class LeaveRoomPresenterImpl @Inject constructor( } } -private fun showLeaveRoomAlert( +private suspend fun showLeaveRoomAlert( matrixClient: MatrixClient, roomId: RoomId, confirmation: MutableState, diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt index 7d752ea8b6..aa44b4f001 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt @@ -172,7 +172,7 @@ class RoomListPresenter @Inject constructor( // Safe to give bigger size than room list val extendedRangeEnd = range.last + midExtendedRangeSize val extendedRange = IntRange(extendedRangeStart, extendedRangeEnd) - client.roomSummaryDataSource.updateRoomListVisibleRange(extendedRange) + client.roomSummaryDataSource.updateAllRoomsVisibleRange(extendedRange) } private suspend fun mapRoomSummaries( 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 84855adb96..ca03263618 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,14 +31,16 @@ import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import kotlinx.coroutines.TimeoutCancellationException import java.io.Closeable +import kotlin.time.Duration interface MatrixClient : Closeable { val sessionId: SessionId val roomSummaryDataSource: RoomSummaryDataSource val mediaLoader: MatrixMediaLoader - fun getRoom(roomId: RoomId): MatrixRoom? - fun findDM(userId: UserId): MatrixRoom? + suspend fun getRoom(roomId: RoomId): MatrixRoom? + suspend fun findDM(userId: UserId): MatrixRoom? suspend fun ignoreUser(userId: UserId): Result suspend fun unignoreUser(userId: UserId): Result suspend fun createRoom(createRoomParams: CreateRoomParameters): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomSummaryDataSource.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomSummaryDataSource.kt index e0aaecd6a9..d8e67a40c3 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomSummaryDataSource.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomSummaryDataSource.kt @@ -25,8 +25,8 @@ interface RoomSummaryDataSource { data class Loaded(val numberOfRooms: Int): LoadingState() } + fun updateAllRoomsVisibleRange(range: IntRange) fun allRoomsLoadingState(): StateFlow fun allRooms(): StateFlow> fun inviteRooms(): StateFlow> - fun updateRoomListVisibleRange(range: IntRange) } 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 6cfa71d097..84a5a38784 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 @@ -57,12 +57,15 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientDelegate +import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.io.File @@ -91,7 +94,6 @@ class RustMatrixClient constructor( ) private val notificationService = RustNotificationService(client) - private val clientDelegate = object : ClientDelegate { override fun didReceiveAuthError(isSoftLogout: Boolean) { //TODO handle this @@ -127,9 +129,16 @@ class RustMatrixClient constructor( }.launchIn(sessionCoroutineScope) } - override fun getRoom(roomId: RoomId): MatrixRoom? { - val roomListItem = roomListService.roomOrNull(roomId.value) ?: return null - val fullRoom = roomListItem.fullRoom() + override suspend fun getRoom(roomId: RoomId): MatrixRoom? { + var cachedPairOfRoom = pairOfRoom(roomId) + if (cachedPairOfRoom == null) { + roomSummaryDataSource.allRoomsLoadingState().firstOrNull { + it is RoomSummaryDataSource.LoadingState.Loaded + } + cachedPairOfRoom = pairOfRoom(roomId) + } + if (cachedPairOfRoom == null) return null + val (roomListItem, fullRoom) = cachedPairOfRoom return RustMatrixRoom( sessionId = sessionId, roomListItem = roomListItem, @@ -141,7 +150,19 @@ class RustMatrixClient constructor( ) } - override fun findDM(userId: UserId): MatrixRoom? { + private suspend fun pairOfRoom(roomId: RoomId): Pair? { + Timber.v("Resume get pair of room for $roomId") + val cachedRoomListItem = roomListService.roomOrNull(roomId.value) + val fullRoom = cachedRoomListItem?.fullRoom() + Timber.v("Finish get pair of room for $roomId") + return if (cachedRoomListItem == null || fullRoom == null) { + null + } else { + Pair(cachedRoomListItem, fullRoom) + } + } + + override suspend fun findDM(userId: UserId): MatrixRoom? { val roomId = client.getDmRoom(userId.value)?.use { RoomId(it.id()) } return roomId?.let { getRoom(it) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomListExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomListExtensions.kt index a6569158d0..824d477fd3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomListExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomListExtensions.kt @@ -25,6 +25,7 @@ import org.matrix.rustcomponents.sdk.RoomList import org.matrix.rustcomponents.sdk.RoomListEntriesListener import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate import org.matrix.rustcomponents.sdk.RoomListEntry +import org.matrix.rustcomponents.sdk.RoomListException import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListLoadingState import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener @@ -60,8 +61,8 @@ fun RoomList.entriesFlow(onInitialList: suspend (List) -> Unit): fun RoomListService.roomOrNull(roomId: String): RoomListItem? { return try { room(roomId) - } catch (failure: Throwable) { - Timber.e(failure, "Failed finding room with id=$roomId") + } catch (exception: RoomListException) { + Timber.e(exception, "Failed finding room with id=$roomId") return null } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt index 29e3989563..3da4c560e8 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt @@ -90,7 +90,7 @@ internal class RustRoomSummaryDataSource( return allRoomsLoadingState } - override fun updateRoomListVisibleRange(range: IntRange) { + override fun updateAllRoomsVisibleRange(range: IntRange) { Timber.v("setVisibleRange=$range") sessionCoroutineScope.launch { try { 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 cfc6e14fd8..4e43df2d2c 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 @@ -68,7 +68,7 @@ class FakeMatrixClient( return getRoomResults[roomId] } - override fun findDM(userId: UserId): MatrixRoom? { + override suspend fun findDM(userId: UserId): MatrixRoom? { return findDmResult } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeRoomSummaryDataSource.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeRoomSummaryDataSource.kt index 87169aa498..cae36e14c8 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeRoomSummaryDataSource.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeRoomSummaryDataSource.kt @@ -54,7 +54,7 @@ class FakeRoomSummaryDataSource : RoomSummaryDataSource { var latestSlidingSyncRange: IntRange? = null private set - override fun updateRoomListVisibleRange(range: IntRange) { + override fun updateAllRoomsVisibleRange(range: IntRange) { latestSlidingSyncRange = range } }