Resolve RoomId in RoomFlowNode.

This commit is contained in:
Benoit Marty 2024-04-16 15:16:53 +02:00 committed by Benoit Marty
parent c1bb910dbd
commit c0bd527486
9 changed files with 98 additions and 51 deletions

View file

@ -43,17 +43,19 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
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.components.async.AsyncFailure
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient 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.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership 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.RoomMembershipObserver
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.util.Optional import java.util.Optional
@ -87,43 +89,72 @@ class RoomFlowNode @AssistedInject constructor(
data object Loading : NavTarget data object Loading : NavTarget
@Parcelize @Parcelize
data object JoinRoom : NavTarget data class JoinRoom(val roomId: RoomId) : NavTarget
@Parcelize
data class RoomAliasError(val roomAlias: RoomAlias) : NavTarget
@Parcelize @Parcelize
data class JoinedRoom(val roomId: RoomId) : NavTarget data class JoinedRoom(val roomId: RoomId) : NavTarget
} }
private var roomMembershipJob: Job? = null
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
resolveRoomId()
}
private fun resolveRoomId() {
lifecycleScope.launch {
when (val i = inputs.roomIdOrAlias) {
is RoomIdOrAlias.Alias -> {
client.resolveRoomAlias(i.roomAlias)
.onFailure {
Timber.e(it, "Failed to resolve room alias")
backstack.newRoot(NavTarget.RoomAliasError(i.roomAlias))
}
.onSuccess {
subscribeToRoomInfoFlow(it)
}
}
is RoomIdOrAlias.Id -> {
subscribeToRoomInfoFlow(i.roomId)
}
}
}
}
private fun subscribeToRoomInfoFlow(roomId: RoomId) {
client.getRoomInfoFlow( client.getRoomInfoFlow(
inputs.roomIdOrAlias roomId
).onEach { roomInfo -> ).onEach { roomInfo ->
Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}") Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}")
val info = roomInfo.getOrNull() val info = roomInfo.getOrNull()
if (info?.currentUserMembership == CurrentUserMembership.JOINED) { if (info?.currentUserMembership == CurrentUserMembership.JOINED) {
backstack.newRoot(NavTarget.JoinedRoom(info.id)) backstack.newRoot(NavTarget.JoinedRoom(roomId))
// When leaving the room from this session only, navigate up.
roomMembershipJob?.cancel()
roomMembershipJob = roomMembershipObserver.updates
.filter { update -> update.roomId == info.id && !update.isUserInRoom }
.onEach {
navigateUp()
}
.launchIn(lifecycleScope)
} else { } else {
backstack.newRoot(NavTarget.JoinRoom) backstack.newRoot(NavTarget.JoinRoom(roomId))
} }
} }
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
// When leaving the room from this session only, navigate up.
roomMembershipObserver.updates
.filter { update -> update.roomId == roomId && !update.isUserInRoom }
.onEach {
navigateUp()
}
.launchIn(lifecycleScope)
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
NavTarget.Loading -> loadingNode(buildContext) NavTarget.Loading -> loadingNode(buildContext)
NavTarget.JoinRoom -> { is NavTarget.JoinRoom -> {
val inputs = JoinRoomEntryPoint.Inputs(inputs.roomIdOrAlias, roomDescription = inputs.roomDescription) val inputs = JoinRoomEntryPoint.Inputs(
roomId = navTarget.roomId,
roomIdOrAlias = inputs.roomIdOrAlias,
roomDescription = inputs.roomDescription,
)
joinRoomEntryPoint.createNode(this, buildContext, inputs) joinRoomEntryPoint.createNode(this, buildContext, inputs)
} }
is NavTarget.JoinedRoom -> { is NavTarget.JoinedRoom -> {
@ -131,6 +162,7 @@ class RoomFlowNode @AssistedInject constructor(
val inputs = JoinedRoomFlowNode.Inputs(navTarget.roomId, initialElement = inputs.initialElement) val inputs = JoinedRoomFlowNode.Inputs(navTarget.roomId, initialElement = inputs.initialElement)
createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback) createNode<JoinedRoomFlowNode>(buildContext, plugins = listOf(inputs) + roomFlowNodeCallback)
} }
is NavTarget.RoomAliasError -> roomAliasErrorNode(buildContext, navTarget.roomAlias)
} }
} }
@ -140,6 +172,15 @@ class RoomFlowNode @AssistedInject constructor(
} }
} }
private fun roomAliasErrorNode(buildContext: BuildContext, roomAlias: RoomAlias) = node(buildContext) {
Box(modifier = it.fillMaxSize(), contentAlignment = Alignment.Center) {
AsyncFailure(
throwable = Exception("Unable to resolve alias ${roomAlias.value}"),
onRetry = { resolveRoomId() },
)
}
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
BackstackView(transitionHandler = JumpToEndTransitionHandler()) BackstackView(transitionHandler = JumpToEndTransitionHandler())

View file

@ -21,6 +21,7 @@ import com.bumble.appyx.core.node.Node
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.FeatureEntryPoint import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.NodeInputs
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.RoomIdOrAlias
import java.util.Optional import java.util.Optional
@ -28,6 +29,7 @@ interface JoinRoomEntryPoint : FeatureEntryPoint {
fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs): Node fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs): Node
data class Inputs( data class Inputs(
val roomId: RoomId,
val roomIdOrAlias: RoomIdOrAlias, val roomIdOrAlias: RoomIdOrAlias,
val roomDescription: Optional<RoomDescription>, val roomDescription: Optional<RoomDescription>,
) : NodeInputs ) : NodeInputs

View file

@ -37,7 +37,11 @@ class JoinRoomNode @AssistedInject constructor(
private val acceptDeclineInviteView: AcceptDeclineInviteView, private val acceptDeclineInviteView: AcceptDeclineInviteView,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
private val inputs: JoinRoomEntryPoint.Inputs = inputs() private val inputs: JoinRoomEntryPoint.Inputs = inputs()
private val presenter = presenterFactory.create(inputs.roomIdOrAlias, inputs.roomDescription) private val presenter = presenterFactory.create(
inputs.roomId,
inputs.roomIdOrAlias,
inputs.roomDescription,
)
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {

View file

@ -30,6 +30,7 @@ import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient 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.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
@ -38,19 +39,24 @@ import kotlinx.coroutines.launch
import java.util.Optional import java.util.Optional
class JoinRoomPresenter @AssistedInject constructor( class JoinRoomPresenter @AssistedInject constructor(
@Assisted private val roomId: RoomId,
@Assisted private val roomIdOrAlias: RoomIdOrAlias, @Assisted private val roomIdOrAlias: RoomIdOrAlias,
@Assisted private val roomDescription: Optional<RoomDescription>, @Assisted private val roomDescription: Optional<RoomDescription>,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>, private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
) : Presenter<JoinRoomState> { ) : Presenter<JoinRoomState> {
interface Factory { interface Factory {
fun create(roomIdOrAlias: RoomIdOrAlias, roomDescription: Optional<RoomDescription>): JoinRoomPresenter fun create(
roomId: RoomId,
roomIdOrAlias: RoomIdOrAlias,
roomDescription: Optional<RoomDescription>,
): JoinRoomPresenter
} }
@Composable @Composable
override fun present(): JoinRoomState { override fun present(): JoinRoomState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val roomInfo by matrixClient.getRoomInfoFlow(roomIdOrAlias).collectAsState(initial = Optional.empty()) val roomInfo by matrixClient.getRoomInfoFlow(roomId).collectAsState(initial = Optional.empty())
val contentState by produceState<ContentState>(initialValue = ContentState.Loading(roomIdOrAlias), key1 = roomInfo) { val contentState by produceState<ContentState>(initialValue = ContentState.Loading(roomIdOrAlias), key1 = roomInfo) {
value = when { value = when {
roomInfo.isPresent -> { roomInfo.isPresent -> {

View file

@ -25,6 +25,7 @@ import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient 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.RoomIdOrAlias
import java.util.Optional import java.util.Optional
@ -37,8 +38,13 @@ object JoinRoomModule {
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>, acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
): JoinRoomPresenter.Factory { ): JoinRoomPresenter.Factory {
return object : JoinRoomPresenter.Factory { return object : JoinRoomPresenter.Factory {
override fun create(roomIdOrAlias: RoomIdOrAlias, roomDescription: Optional<RoomDescription>): JoinRoomPresenter { override fun create(
roomId: RoomId,
roomIdOrAlias: RoomIdOrAlias,
roomDescription: Optional<RoomDescription>,
): JoinRoomPresenter {
return JoinRoomPresenter( return JoinRoomPresenter(
roomId = roomId,
roomIdOrAlias = roomIdOrAlias, roomIdOrAlias = roomIdOrAlias,
roomDescription = roomDescription, roomDescription = roomDescription,
matrixClient = client, matrixClient = client,

View file

@ -23,7 +23,9 @@ import io.element.android.features.invite.api.response.anAcceptDeclineInviteStat
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient 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.RoomId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.test.A_ROOM_ID 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_ROOM_NAME
@ -49,7 +51,7 @@ class JoinRoomPresenterTest {
val presenter = createJoinRoomPresenter() val presenter = createJoinRoomPresenter()
presenter.test { presenter.test {
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID)) assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias()))
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown) assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.Unknown)
assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState()) assertThat(state.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
} }
@ -245,6 +247,7 @@ class JoinRoomPresenterTest {
): JoinRoomPresenter { ): JoinRoomPresenter {
return JoinRoomPresenter( return JoinRoomPresenter(
roomId = roomId, roomId = roomId,
roomIdOrAlias = roomId.toRoomIdOrAlias(),
roomDescription = roomDescription, roomDescription = roomDescription,
matrixClient = matrixClient, matrixClient = matrixClient,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
@ -255,7 +258,7 @@ class JoinRoomPresenterTest {
roomId: RoomId = A_ROOM_ID, roomId: RoomId = A_ROOM_ID,
name: String? = A_ROOM_NAME, name: String? = A_ROOM_NAME,
topic: String? = "A room about something", topic: String? = "A room about something",
alias: String? = "#alias:matrix.org", alias: RoomAlias? = RoomAlias("#alias:matrix.org"),
avatarUrl: String? = null, avatarUrl: String? = null,
joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN, joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN,
numberOfMembers: Long = 2L numberOfMembers: Long = 2L

View file

@ -95,7 +95,7 @@ interface MatrixClient : Closeable {
suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?> suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?>
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String> suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
fun roomMembershipObserver(): RoomMembershipObserver fun roomMembershipObserver(): RoomMembershipObserver
fun getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<MatrixRoomInfo>> fun getRoomInfoFlow(roomId: RoomId): Flow<Optional<MatrixRoomInfo>>
fun isMe(userId: UserId?) = userId == sessionId fun isMe(userId: UserId?) = userId == sessionId

View file

@ -562,33 +562,18 @@ class RustMatrixClient(
override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver override fun roomMembershipObserver(): RoomMembershipObserver = roomMembershipObserver
override fun getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<MatrixRoomInfo>> { override fun getRoomInfoFlow(roomId: RoomId): Flow<Optional<MatrixRoomInfo>> {
return flow { return flow {
val roomId = when (roomIdOrAlias) { var room = getRoom(roomId)
is RoomIdOrAlias.Alias -> { if (room == null) {
resolveRoomAlias(roomIdOrAlias.roomAlias) emit(Optional.empty())
.onFailure { awaitRoom(roomId, INFINITE)
// TODO Get a way to emit an error room = getRoom(roomId)
Timber.e("Unable to resolve room alias ${roomIdOrAlias.roomAlias}")
emit(Optional.empty())
return@flow
}
.getOrNull()
}
is RoomIdOrAlias.Id -> roomIdOrAlias.roomId
} }
if (roomId != null) { room?.use {
var room = getRoom(roomId) room.roomInfoFlow
if (room == null) { .map { roomInfo -> Optional.of(roomInfo) }
emit(Optional.empty()) .collect(this)
awaitRoom(roomId, INFINITE)
room = getRoom(roomId)
}
room?.use {
room.roomInfoFlow
.map { roomInfo -> Optional.of(roomInfo) }
.collect(this)
}
} }
}.distinctUntilChanged() }.distinctUntilChanged()
} }

View file

@ -107,7 +107,7 @@ class FakeMatrixClient(
Result.success(it) Result.success(it)
} }
var getRoomInfoFlowLambda = { _: RoomIdOrAlias -> var getRoomInfoFlowLambda = { _: RoomId ->
flowOf<Optional<MatrixRoomInfo>>(Optional.empty()) flowOf<Optional<MatrixRoomInfo>>(Optional.empty())
} }
@ -293,5 +293,5 @@ class FakeMatrixClient(
return Result.success(visitedRoomsId) return Result.success(visitedRoomsId)
} }
override fun getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias) = getRoomInfoFlowLambda(roomIdOrAlias) override fun getRoomInfoFlow(roomId: RoomId) = getRoomInfoFlowLambda(roomId)
} }