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 @@
/*
* 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
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator 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.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -147,6 +146,7 @@ class LoggedInFlowNode @AssistedInject constructor(
observeAnalyticsState() observeAnalyticsState()
lifecycle.subscribe( lifecycle.subscribe(
onCreate = { onCreate = {
syncService.startSync()
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) } plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) }
val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory() val imageLoaderFactory = bindings<MatrixUIBindings>().loggedInImageLoaderFactory()
Coil.setImageLoader(imageLoaderFactory) Coil.setImageLoader(imageLoaderFactory)
@ -267,24 +267,14 @@ class LoggedInFlowNode @AssistedInject constructor(
.build() .build()
} }
is NavTarget.Room -> { 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 nodeLifecycleCallbacks = plugins<NodeLifecycleCallback>()
val callback = object : RoomFlowNode.Callback { val callback = object : RoomFlowNode.Callback {
override fun onForwardedToSingleRoom(roomId: RoomId) { override fun onForwardedToSingleRoom(roomId: RoomId) {
coroutineScope.launch { attachRoom(roomId) } coroutineScope.launch { attachRoom(roomId) }
} }
} }
val inputs = RoomFlowNode.Inputs(room, initialElement = navTarget.initialElement) val inputs = AwaitRoomNode.Inputs(roomId = navTarget.roomId, initialElement = navTarget.initialElement)
createNode<RoomFlowNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks) createNode<AwaitRoomNode>(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks)
}
} }
NavTarget.Settings -> { NavTarget.Settings -> {
val callback = object : PreferencesEntryPoint.Callback { 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 { return attachChild {
backstack.singleTop(NavTarget.RoomList) backstack.singleTop(NavTarget.RoomList)
backstack.push(NavTarget.Room(roomId)) 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(
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()
val startDmAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) } 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) { fun handleEvents(event: CreateRoomRootEvents) {
when (event) { when (event) {
is CreateRoomRootEvents.StartDM -> startDm(event.matrixUser) is CreateRoomRootEvents.StartDM -> localCoroutineScope.startDm(event.matrixUser, startDmAction)
CreateRoomRootEvents.CancelStartDM -> startDmAction.value = Async.Uninitialized CreateRoomRootEvents.CancelStartDM -> startDmAction.value = Async.Uninitialized
} }
} }
@ -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 { suspend {
matrixClient.createDM(user.userId).getOrThrow() matrixClient.findDM(matrixUser.userId).use { existingDM ->
.also { analyticsService.capture(CreatedRoom(isDM = true)) } existingDM?.roomId ?: createDM(matrixUser)
}
}.runCatchingUpdatingState(startDmAction) }.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(
} }
} }
private fun showLeaveRoomAlert( private suspend fun showLeaveRoomAlert(
matrixClient: MatrixClient, matrixClient: MatrixClient,
roomId: RoomId, roomId: RoomId,
confirmation: MutableState<LeaveRoomState.Confirmation>, 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(
// Safe to give bigger size than room list // Safe to give bigger size than room list
val extendedRangeEnd = range.last + midExtendedRangeSize val extendedRangeEnd = range.last + midExtendedRangeSize
val extendedRange = IntRange(extendedRangeStart, extendedRangeEnd) val extendedRange = IntRange(extendedRangeStart, extendedRangeEnd)
client.roomSummaryDataSource.updateRoomListVisibleRange(extendedRange) client.roomSummaryDataSource.updateAllRoomsVisibleRange(extendedRange)
} }
private suspend fun mapRoomSummaries( 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
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults 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.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import kotlinx.coroutines.TimeoutCancellationException
import java.io.Closeable import java.io.Closeable
import kotlin.time.Duration
interface MatrixClient : Closeable { interface MatrixClient : Closeable {
val sessionId: SessionId val sessionId: SessionId
val roomSummaryDataSource: RoomSummaryDataSource val roomSummaryDataSource: RoomSummaryDataSource
val mediaLoader: MatrixMediaLoader val mediaLoader: MatrixMediaLoader
fun getRoom(roomId: RoomId): MatrixRoom? suspend fun getRoom(roomId: RoomId): MatrixRoom?
fun findDM(userId: UserId): MatrixRoom? suspend fun findDM(userId: UserId): MatrixRoom?
suspend fun ignoreUser(userId: UserId): Result<Unit> suspend fun ignoreUser(userId: UserId): Result<Unit>
suspend fun unignoreUser(userId: UserId): Result<Unit> suspend fun unignoreUser(userId: UserId): Result<Unit>
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> 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 {
data class Loaded(val numberOfRooms: Int): LoadingState() data class Loaded(val numberOfRooms: Int): LoadingState()
} }
fun updateAllRoomsVisibleRange(range: IntRange)
fun allRoomsLoadingState(): StateFlow<LoadingState> fun allRoomsLoadingState(): StateFlow<LoadingState>
fun allRooms(): StateFlow<List<RoomSummary>> fun allRooms(): StateFlow<List<RoomSummary>>
fun inviteRooms(): 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
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.ClientDelegate 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 org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -91,7 +94,6 @@ class RustMatrixClient constructor(
) )
private val notificationService = RustNotificationService(client) private val notificationService = RustNotificationService(client)
private val clientDelegate = object : ClientDelegate { private val clientDelegate = object : ClientDelegate {
override fun didReceiveAuthError(isSoftLogout: Boolean) { override fun didReceiveAuthError(isSoftLogout: Boolean) {
//TODO handle this //TODO handle this
@ -127,9 +129,16 @@ class RustMatrixClient constructor(
}.launchIn(sessionCoroutineScope) }.launchIn(sessionCoroutineScope)
} }
override fun getRoom(roomId: RoomId): MatrixRoom? { override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
val roomListItem = roomListService.roomOrNull(roomId.value) ?: return null var cachedPairOfRoom = pairOfRoom(roomId)
val fullRoom = roomListItem.fullRoom() 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( return RustMatrixRoom(
sessionId = sessionId, sessionId = sessionId,
roomListItem = roomListItem, roomListItem = roomListItem,
@ -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()) } val roomId = client.getDmRoom(userId.value)?.use { RoomId(it.id()) }
return roomId?.let { getRoom(it) } 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
import org.matrix.rustcomponents.sdk.RoomListEntriesListener import org.matrix.rustcomponents.sdk.RoomListEntriesListener
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListEntry import org.matrix.rustcomponents.sdk.RoomListEntry
import org.matrix.rustcomponents.sdk.RoomListException
import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListItem
import org.matrix.rustcomponents.sdk.RoomListLoadingState import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener import org.matrix.rustcomponents.sdk.RoomListLoadingStateListener
@ -60,8 +61,8 @@ fun RoomList.entriesFlow(onInitialList: suspend (List<RoomListEntry>) -> Unit):
fun RoomListService.roomOrNull(roomId: String): RoomListItem? { fun RoomListService.roomOrNull(roomId: String): RoomListItem? {
return try { return try {
room(roomId) room(roomId)
} catch (failure: Throwable) { } catch (exception: RoomListException) {
Timber.e(failure, "Failed finding room with id=$roomId") Timber.e(exception, "Failed finding room with id=$roomId")
return null 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(
return allRoomsLoadingState return allRoomsLoadingState
} }
override fun updateRoomListVisibleRange(range: IntRange) { override fun updateAllRoomsVisibleRange(range: IntRange) {
Timber.v("setVisibleRange=$range") Timber.v("setVisibleRange=$range")
sessionCoroutineScope.launch { sessionCoroutineScope.launch {
try { try {

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

@ -68,7 +68,7 @@ class FakeMatrixClient(
return getRoomResults[roomId] return getRoomResults[roomId]
} }
override fun findDM(userId: UserId): MatrixRoom? { override suspend fun findDM(userId: UserId): MatrixRoom? {
return findDmResult 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 {
var latestSlidingSyncRange: IntRange? = null var latestSlidingSyncRange: IntRange? = null
private set private set
override fun updateRoomListVisibleRange(range: IntRange) { override fun updateAllRoomsVisibleRange(range: IntRange) {
latestSlidingSyncRange = range latestSlidingSyncRange = range
} }
} }

Loading…
Cancel
Save