Resolve RoomId in RoomFlowNode.

This commit is contained in:
Benoit Marty 2024-04-16 15:16:53 +02:00 committed by Benoit Marty
parent 09faa6d1c9
commit 235ef2a71b
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.createNode
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.di.SessionScope
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.RoomIdOrAlias
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.util.Optional
@ -87,43 +89,72 @@ class RoomFlowNode @AssistedInject constructor(
data object Loading : NavTarget
@Parcelize
data object JoinRoom : NavTarget
data class JoinRoom(val roomId: RoomId) : NavTarget
@Parcelize
data class RoomAliasError(val roomAlias: RoomAlias) : NavTarget
@Parcelize
data class JoinedRoom(val roomId: RoomId) : NavTarget
}
private var roomMembershipJob: Job? = null
override fun 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(
inputs.roomIdOrAlias
roomId
).onEach { roomInfo ->
Timber.d("Room membership: ${roomInfo.map { it.currentUserMembership }}")
val info = roomInfo.getOrNull()
if (info?.currentUserMembership == CurrentUserMembership.JOINED) {
backstack.newRoot(NavTarget.JoinedRoom(info.id))
// 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)
backstack.newRoot(NavTarget.JoinedRoom(roomId))
} else {
backstack.newRoot(NavTarget.JoinRoom)
backstack.newRoot(NavTarget.JoinRoom(roomId))
}
}
.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 {
return when (navTarget) {
NavTarget.Loading -> loadingNode(buildContext)
NavTarget.JoinRoom -> {
val inputs = JoinRoomEntryPoint.Inputs(inputs.roomIdOrAlias, roomDescription = inputs.roomDescription)
is NavTarget.JoinRoom -> {
val inputs = JoinRoomEntryPoint.Inputs(
roomId = navTarget.roomId,
roomIdOrAlias = inputs.roomIdOrAlias,
roomDescription = inputs.roomDescription,
)
joinRoomEntryPoint.createNode(this, buildContext, inputs)
}
is NavTarget.JoinedRoom -> {
@ -131,6 +162,7 @@ class RoomFlowNode @AssistedInject constructor(
val inputs = JoinedRoomFlowNode.Inputs(navTarget.roomId, initialElement = inputs.initialElement)
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
override fun View(modifier: Modifier) {
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.libraries.architecture.FeatureEntryPoint
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 java.util.Optional
@ -28,6 +29,7 @@ interface JoinRoomEntryPoint : FeatureEntryPoint {
fun createNode(parentNode: Node, buildContext: BuildContext, inputs: Inputs): Node
data class Inputs(
val roomId: RoomId,
val roomIdOrAlias: RoomIdOrAlias,
val roomDescription: Optional<RoomDescription>,
) : NodeInputs

View file

@ -37,7 +37,11 @@ class JoinRoomNode @AssistedInject constructor(
private val acceptDeclineInviteView: AcceptDeclineInviteView,
) : Node(buildContext, plugins = plugins) {
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
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.libraries.architecture.Presenter
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.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
@ -38,19 +39,24 @@ import kotlinx.coroutines.launch
import java.util.Optional
class JoinRoomPresenter @AssistedInject constructor(
@Assisted private val roomId: RoomId,
@Assisted private val roomIdOrAlias: RoomIdOrAlias,
@Assisted private val roomDescription: Optional<RoomDescription>,
private val matrixClient: MatrixClient,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
) : Presenter<JoinRoomState> {
interface Factory {
fun create(roomIdOrAlias: RoomIdOrAlias, roomDescription: Optional<RoomDescription>): JoinRoomPresenter
fun create(
roomId: RoomId,
roomIdOrAlias: RoomIdOrAlias,
roomDescription: Optional<RoomDescription>,
): JoinRoomPresenter
}
@Composable
override fun present(): JoinRoomState {
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) {
value = when {
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.di.SessionScope
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 java.util.Optional
@ -37,8 +38,13 @@ object JoinRoomModule {
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
): 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(
roomId = roomId,
roomIdOrAlias = roomIdOrAlias,
roomDescription = roomDescription,
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.libraries.architecture.Presenter
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.toRoomIdOrAlias
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_NAME
@ -49,7 +51,7 @@ class JoinRoomPresenterTest {
val presenter = createJoinRoomPresenter()
presenter.test {
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.acceptDeclineInviteState).isEqualTo(anAcceptDeclineInviteState())
}
@ -245,6 +247,7 @@ class JoinRoomPresenterTest {
): JoinRoomPresenter {
return JoinRoomPresenter(
roomId = roomId,
roomIdOrAlias = roomId.toRoomIdOrAlias(),
roomDescription = roomDescription,
matrixClient = matrixClient,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
@ -255,7 +258,7 @@ class JoinRoomPresenterTest {
roomId: RoomId = A_ROOM_ID,
name: String? = A_ROOM_NAME,
topic: String? = "A room about something",
alias: String? = "#alias:matrix.org",
alias: RoomAlias? = RoomAlias("#alias:matrix.org"),
avatarUrl: String? = null,
joinRule: RoomDescription.JoinRule = RoomDescription.JoinRule.UNKNOWN,
numberOfMembers: Long = 2L

View file

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

View file

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

View file

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