Browse Source

Await room: first attempt to wait for a room to be ready

pull/794/head
ganfra 1 year ago
parent
commit
d59f59e9f6
  1. 129
      appnav/src/main/kotlin/io/element/android/appnav/AwaitRoomNode.kt
  2. 18
      appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
  3. 29
      features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt
  4. 2
      features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImpl.kt
  5. 2
      features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt
  6. 6
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt
  7. 2
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomSummaryDataSource.kt
  8. 31
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt
  9. 5
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomListExtensions.kt
  10. 2
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt
  11. 2
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt
  12. 2
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeRoomSummaryDataSource.kt

129
appnav/src/main/kotlin/io/element/android/appnav/AwaitRoomNode.kt

@ -0,0 +1,129 @@ @@ -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<Plugin>,
private val matrixClient: MatrixClient,
) :
BackstackNode<AwaitRoomNode.NavTarget>(
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<NodeLifecycleCallback>()
val roomFlowNodeCallback = plugins<RoomFlowNode.Callback>()
val room = roomStateFlow.value
if (room == null) {
loadingNode(buildContext)
} else {
val inputs = RoomFlowNode.Inputs(room, initialElement = inputs.initialElement)
createNode<RoomFlowNode>(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,
)
}
}

18
appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

@ -57,7 +57,6 @@ import io.element.android.libraries.architecture.bindings @@ -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( @@ -147,6 +146,7 @@ class LoggedInFlowNode @AssistedInject constructor(
observeAnalyticsState()
lifecycle.subscribe(
onCreate = {
syncService.startSync()
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) }
val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory()
Coil.setImageLoader(imageLoaderFactory)
@ -267,24 +267,14 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -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<NodeLifecycleCallback>()
val callback = object : RoomFlowNode.Callback {
override fun onForwardedToSingleRoom(roomId: RoomId) {
coroutineScope.launch { attachRoom(roomId) }
}
}
val inputs = RoomFlowNode.Inputs(room, initialElement = navTarget.initialElement)
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks)
}
val inputs = AwaitRoomNode.Inputs(roomId = navTarget.roomId, initialElement = navTarget.initialElement)
createNode<AwaitRoomNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks)
}
NavTarget.Settings -> {
val callback = object : PreferencesEntryPoint.Callback {
@ -342,7 +332,7 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -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))

29
features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt

@ -65,20 +65,9 @@ class CreateRoomRootPresenter @Inject constructor( @@ -65,20 +65,9 @@ class CreateRoomRootPresenter @Inject constructor(
val localCoroutineScope = rememberCoroutineScope()
val startDmAction: MutableState<Async<RoomId>> = 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( @@ -91,10 +80,20 @@ class CreateRoomRootPresenter @Inject constructor(
)
}
private fun CoroutineScope.createDM(user: MatrixUser, startDmAction: MutableState<Async<RoomId>>) = launch {
private fun CoroutineScope.startDm(matrixUser: MatrixUser, startDmAction: MutableState<Async<RoomId>>) = 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()
}
}

2
features/leaveroom/impl/src/main/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImpl.kt

@ -78,7 +78,7 @@ class LeaveRoomPresenterImpl @Inject constructor( @@ -78,7 +78,7 @@ class LeaveRoomPresenterImpl @Inject constructor(
}
}
private fun showLeaveRoomAlert(
private suspend fun showLeaveRoomAlert(
matrixClient: MatrixClient,
roomId: RoomId,
confirmation: MutableState<LeaveRoomState.Confirmation>,

2
features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListPresenter.kt

@ -172,7 +172,7 @@ class RoomListPresenter @Inject constructor( @@ -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(

6
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 @@ -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<Unit>
suspend fun unignoreUser(userId: UserId): Result<Unit>
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId>

2
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomSummaryDataSource.kt

@ -25,8 +25,8 @@ interface RoomSummaryDataSource { @@ -25,8 +25,8 @@ interface RoomSummaryDataSource {
data class Loaded(val numberOfRooms: Int): LoadingState()
}
fun updateAllRoomsVisibleRange(range: IntRange)
fun allRoomsLoadingState(): StateFlow<LoadingState>
fun allRooms(): StateFlow<List<RoomSummary>>
fun inviteRooms(): StateFlow<List<RoomSummary>>
fun updateRoomListVisibleRange(range: IntRange)
}

31
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt

@ -57,12 +57,15 @@ import kotlinx.coroutines.Dispatchers @@ -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( @@ -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( @@ -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( @@ -141,7 +150,19 @@ class RustMatrixClient constructor(
)
}
override fun findDM(userId: UserId): MatrixRoom? {
private suspend fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? {
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) }
}

5
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 @@ -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<RoomListEntry>) -> Unit): @@ -60,8 +61,8 @@ fun RoomList.entriesFlow(onInitialList: suspend (List<RoomListEntry>) -> 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
}
}

2
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt

@ -90,7 +90,7 @@ internal class RustRoomSummaryDataSource( @@ -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 {

2
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt

@ -68,7 +68,7 @@ class FakeMatrixClient( @@ -68,7 +68,7 @@ class FakeMatrixClient(
return getRoomResults[roomId]
}
override fun findDM(userId: UserId): MatrixRoom? {
override suspend fun findDM(userId: UserId): MatrixRoom? {
return findDmResult
}

2
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeRoomSummaryDataSource.kt

@ -54,7 +54,7 @@ class FakeRoomSummaryDataSource : RoomSummaryDataSource { @@ -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
}
}

Loading…
Cancel
Save