knock : improve a bit code and add tests.

This commit is contained in:
ganfra 2024-10-23 11:42:46 +02:00
parent 6afc72e453
commit 2ffd46061e
17 changed files with 193 additions and 52 deletions

View file

@ -10,8 +10,9 @@ package io.element.android.features.joinroom.impl
sealed interface JoinRoomEvents { sealed interface JoinRoomEvents {
data object RetryFetchingContent : JoinRoomEvents data object RetryFetchingContent : JoinRoomEvents
data object JoinRoom : JoinRoomEvents data object JoinRoom : JoinRoomEvents
data class KnockRoom(val message: String) : JoinRoomEvents data object KnockRoom : JoinRoomEvents
data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents
data class UpdateKnockMessage(val message: String) : JoinRoomEvents
data object ClearActionStates : JoinRoomEvents data object ClearActionStates : JoinRoomEvents
data object AcceptInvite : JoinRoomEvents data object AcceptInvite : JoinRoomEvents
data object DeclineInvite : JoinRoomEvents data object DeclineInvite : JoinRoomEvents

View file

@ -17,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -24,6 +25,7 @@ import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.InviteData import io.element.android.features.invite.api.response.InviteData
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
import io.element.android.features.joinroom.impl.di.KnockRoom import io.element.android.features.joinroom.impl.di.KnockRoom
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -46,6 +48,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Optional import java.util.Optional
private const val MAX_KNOCK_MESSAGE_LENGTH = 500
class JoinRoomPresenter @AssistedInject constructor( class JoinRoomPresenter @AssistedInject constructor(
@Assisted private val roomId: RoomId, @Assisted private val roomId: RoomId,
@Assisted private val roomIdOrAlias: RoomIdOrAlias, @Assisted private val roomIdOrAlias: RoomIdOrAlias,
@ -55,6 +59,7 @@ class JoinRoomPresenter @AssistedInject constructor(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val joinRoom: JoinRoom, private val joinRoom: JoinRoom,
private val knockRoom: KnockRoom, private val knockRoom: KnockRoom,
private val cancelKnockRoom: CancelKnockRoom,
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>, private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
) : Presenter<JoinRoomState> { ) : Presenter<JoinRoomState> {
@ -76,6 +81,7 @@ class JoinRoomPresenter @AssistedInject constructor(
val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val joinAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val knockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val cancelKnockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) } val cancelKnockAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
var knockMessage by rememberSaveable { mutableStateOf("") }
val contentState by produceState<ContentState>( val contentState by produceState<ContentState>(
initialValue = ContentState.Loading(roomIdOrAlias), initialValue = ContentState.Loading(roomIdOrAlias),
key1 = roomInfo, key1 = roomInfo,
@ -111,7 +117,7 @@ class JoinRoomPresenter @AssistedInject constructor(
fun handleEvents(event: JoinRoomEvents) { fun handleEvents(event: JoinRoomEvents) {
when (event) { when (event) {
JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction) JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, event.message) is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
JoinRoomEvents.AcceptInvite -> { JoinRoomEvents.AcceptInvite -> {
val inviteData = contentState.toInviteData() ?: return val inviteData = contentState.toInviteData() ?: return
acceptDeclineInviteState.eventSink( acceptDeclineInviteState.eventSink(
@ -133,6 +139,9 @@ class JoinRoomPresenter @AssistedInject constructor(
joinAction.value = AsyncAction.Uninitialized joinAction.value = AsyncAction.Uninitialized
cancelKnockAction.value = AsyncAction.Uninitialized cancelKnockAction.value = AsyncAction.Uninitialized
} }
is JoinRoomEvents.UpdateKnockMessage -> {
knockMessage = event.message.take(MAX_KNOCK_MESSAGE_LENGTH)
}
} }
} }
@ -143,6 +152,7 @@ class JoinRoomPresenter @AssistedInject constructor(
knockAction = knockAction.value, knockAction = knockAction.value,
cancelKnockAction = cancelKnockAction.value, cancelKnockAction = cancelKnockAction.value,
applicationName = buildMeta.applicationName, applicationName = buildMeta.applicationName,
knockMessage = knockMessage,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }
@ -159,7 +169,7 @@ class JoinRoomPresenter @AssistedInject constructor(
private fun CoroutineScope.knockRoom(knockAction: MutableState<AsyncAction<Unit>>, message: String) = launch { private fun CoroutineScope.knockRoom(knockAction: MutableState<AsyncAction<Unit>>, message: String) = launch {
knockAction.runUpdatingState { knockAction.runUpdatingState {
knockRoom(roomId) knockRoom(roomIdOrAlias, message, serverNames)
} }
} }
@ -167,15 +177,8 @@ class JoinRoomPresenter @AssistedInject constructor(
if (requiresConfirmation) { if (requiresConfirmation) {
cancelKnockAction.value = AsyncAction.ConfirmingNoParams cancelKnockAction.value = AsyncAction.ConfirmingNoParams
} else { } else {
val room = matrixClient.getRoom(roomId) cancelKnockAction.runUpdatingState {
if (room == null) { cancelKnockRoom(roomId)
cancelKnockAction.value = AsyncAction.Failure(RuntimeException())
} else {
room.use {
cancelKnockAction.runUpdatingState {
room.leave()
}
}
} }
} }
} }

View file

@ -26,6 +26,7 @@ data class JoinRoomState(
val knockAction: AsyncAction<Unit>, val knockAction: AsyncAction<Unit>,
val cancelKnockAction: AsyncAction<Unit>, val cancelKnockAction: AsyncAction<Unit>,
val applicationName: String, val applicationName: String,
val knockMessage: String,
val eventSink: (JoinRoomEvents) -> Unit val eventSink: (JoinRoomEvents) -> Unit
) { ) {
val joinAuthorisationStatus = when (contentState) { val joinAuthorisationStatus = when (contentState) {

View file

@ -131,6 +131,7 @@ fun aJoinRoomState(
joinAction: AsyncAction<Unit> = AsyncAction.Uninitialized, joinAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
knockAction: AsyncAction<Unit> = AsyncAction.Uninitialized, knockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
cancelKnockAction: AsyncAction<Unit> = AsyncAction.Uninitialized, cancelKnockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
knockMessage: String = "",
eventSink: (JoinRoomEvents) -> Unit = {} eventSink: (JoinRoomEvents) -> Unit = {}
) = JoinRoomState( ) = JoinRoomState(
contentState = contentState, contentState = contentState,
@ -139,6 +140,7 @@ fun aJoinRoomState(
knockAction = knockAction, knockAction = knockAction,
cancelKnockAction = cancelKnockAction, cancelKnockAction = cancelKnockAction,
applicationName = "AppName", applicationName = "AppName",
knockMessage = knockMessage,
eventSink = eventSink eventSink = eventSink
) )

View file

@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@ -27,10 +26,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -85,7 +80,6 @@ fun JoinRoomView(
Box( Box(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
) { ) {
var knockMessage by rememberSaveable { mutableStateOf("") }
LightGradientBackground() LightGradientBackground()
HeaderFooterPage( HeaderFooterPage(
containerColor = Color.Transparent, containerColor = Color.Transparent,
@ -97,8 +91,8 @@ fun JoinRoomView(
JoinRoomContent( JoinRoomContent(
contentState = state.contentState, contentState = state.contentState,
applicationName = state.applicationName, applicationName = state.applicationName,
knockMessage = knockMessage, knockMessage = state.knockMessage,
onKnockMessageUpdate = { knockMessage = it }, onKnockMessageUpdate = { state.eventSink(JoinRoomEvents.UpdateKnockMessage(it)) },
) )
}, },
footer = { footer = {
@ -114,7 +108,7 @@ fun JoinRoomView(
state.eventSink(JoinRoomEvents.JoinRoom) state.eventSink(JoinRoomEvents.JoinRoom)
}, },
onKnockRoom = { onKnockRoom = {
state.eventSink(JoinRoomEvents.KnockRoom(knockMessage)) state.eventSink(JoinRoomEvents.KnockRoom)
}, },
onCancelKnock = { onCancelKnock = {
state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = true)) state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = true))
@ -169,9 +163,11 @@ private fun JoinRoomFooter(
onGoBack: () -> Unit, onGoBack: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Box(modifier = modifier Box(
.fillMaxWidth() modifier = modifier
.padding(top = 8.dp)) { .fillMaxWidth()
.padding(top = 8.dp)
) {
if (state.contentState is ContentState.Failure) { if (state.contentState is ContentState.Failure) {
Button( Button(
text = stringResource(CommonStrings.action_retry), text = stringResource(CommonStrings.action_retry),
@ -322,7 +318,7 @@ private fun JoinRoomContent(
} }
@Composable @Composable
fun IsKnockedLoadedContent(modifier: Modifier = Modifier) { private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
BoxWithConstraints( BoxWithConstraints(
modifier = modifier modifier = modifier
.fillMaxHeight() .fillMaxHeight()
@ -397,9 +393,9 @@ private fun DefaultLoadedContent(
OutlinedTextField( OutlinedTextField(
value = knockMessage, value = knockMessage,
onValueChange = onKnockMessageUpdate, onValueChange = onKnockMessageUpdate,
modifier = Modifier maxLines = 3,
.fillMaxWidth() minLines = 3,
.heightIn(min = 90.dp) modifier = Modifier.fillMaxWidth()
) )
Text( Text(
text = stringResource(R.string.screen_join_room_knock_message_description), text = stringResource(R.string.screen_join_room_knock_message_description),

View file

@ -0,0 +1,28 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.joinroom.impl.di
import com.squareup.anvil.annotations.ContributesBinding
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 javax.inject.Inject
interface CancelKnockRoom {
suspend operator fun invoke(roomId: RoomId): Result<Unit>
}
@ContributesBinding(SessionScope::class)
class DefaultCancelKnockRoom @Inject constructor(private val client: MatrixClient) : CancelKnockRoom {
override suspend fun invoke(roomId: RoomId): Result<Unit> {
return client
.getPendingRoom(roomId)
?.leave()
?: Result.failure(IllegalStateException("No pending room found"))
}
}

View file

@ -31,6 +31,7 @@ object JoinRoomModule {
client: MatrixClient, client: MatrixClient,
joinRoom: JoinRoom, joinRoom: JoinRoom,
knockRoom: KnockRoom, knockRoom: KnockRoom,
cancelKnockRoom: CancelKnockRoom,
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>, acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
buildMeta: BuildMeta, buildMeta: BuildMeta,
): JoinRoomPresenter.Factory { ): JoinRoomPresenter.Factory {
@ -51,6 +52,7 @@ object JoinRoomModule {
matrixClient = client, matrixClient = client,
joinRoom = joinRoom, joinRoom = joinRoom,
knockRoom = knockRoom, knockRoom = knockRoom,
cancelKnockRoom = cancelKnockRoom,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter, acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
buildMeta = buildMeta, buildMeta = buildMeta,
) )

View file

@ -10,18 +10,26 @@ package io.element.android.features.joinroom.impl.di
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
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 javax.inject.Inject import javax.inject.Inject
interface KnockRoom { interface KnockRoom {
suspend operator fun invoke(roomId: RoomId): Result<Unit> suspend operator fun invoke(
roomIdOrAlias: RoomIdOrAlias,
message: String,
serverNames: List<String>,
): Result<Unit>
} }
@ContributesBinding(SessionScope::class) @ContributesBinding(SessionScope::class)
class DefaultKnockRoom @Inject constructor(private val client: MatrixClient) : KnockRoom { class DefaultKnockRoom @Inject constructor(private val client: MatrixClient) : KnockRoom {
override suspend fun invoke(roomId: RoomId): Result<Unit> { override suspend fun invoke(
roomIdOrAlias: RoomIdOrAlias,
message: String,
serverNames: List<String>
): Result<Unit> {
return client return client
.knockRoom(roomId) .knockRoom(roomIdOrAlias, message, serverNames)
.map { } .map { }
} }
} }

View file

@ -0,0 +1,20 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.joinroom.impl
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.tests.testutils.simulateLongTask
class FakeCancelKnockRoom(
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) }
) : CancelKnockRoom {
override suspend fun invoke(roomId: RoomId) = simulateLongTask {
lambda(roomId)
}
}

View file

@ -8,13 +8,13 @@
package io.element.android.features.joinroom.impl package io.element.android.features.joinroom.impl
import io.element.android.features.joinroom.impl.di.KnockRoom import io.element.android.features.joinroom.impl.di.KnockRoom
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
class FakeKnockRoom( class FakeKnockRoom(
var lambda: (RoomId) -> Result<Unit> = { Result.success(Unit) } var lambda: (RoomIdOrAlias, String, List<String>) -> Result<Unit> = { _, _, _ -> Result.success(Unit) }
) : KnockRoom { ) : KnockRoom {
override suspend fun invoke(roomId: RoomId) = simulateLongTask { override suspend fun invoke(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<Unit> = simulateLongTask {
lambda(roomId) lambda(roomIdOrAlias, message, serverNames)
} }
} }

View file

@ -12,6 +12,7 @@ import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.response.AcceptDeclineInviteState import io.element.android.features.invite.api.response.AcceptDeclineInviteState
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
import io.element.android.features.joinroom.impl.di.KnockRoom import io.element.android.features.joinroom.impl.di.KnockRoom
import io.element.android.features.roomdirectory.api.RoomDescription import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
@ -37,6 +38,7 @@ import io.element.android.libraries.matrix.test.room.aRoomSummary
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
import io.element.android.libraries.matrix.ui.model.toInviteSender import io.element.android.libraries.matrix.ui.model.toInviteSender
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
@ -59,6 +61,8 @@ class JoinRoomPresenterTest {
assertThat(state.contentState).isEqualTo(ContentState.Loading(A_ROOM_ID.toRoomIdOrAlias())) 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())
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(state.knockAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(state.applicationName).isEqualTo("AppName") assertThat(state.applicationName).isEqualTo("AppName")
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
@ -325,16 +329,20 @@ class JoinRoomPresenterTest {
@Test @Test
fun `present - emit knock room event`() = runTest { fun `present - emit knock room event`() = runTest {
val knockRoomSuccess = lambdaRecorder { _: RoomId -> val knockMessage = "Knock message"
val knockRoomSuccess = lambdaRecorder { _: RoomIdOrAlias, _: String, _: List<String> ->
Result.success(Unit) Result.success(Unit)
} }
val knockRoomFailure = lambdaRecorder { roomId: RoomId -> val knockRoomFailure = lambdaRecorder { roomIdOrAlias: RoomIdOrAlias, _: String, _: List<String> ->
Result.failure<Unit>(RuntimeException("Failed to knock room $roomId")) Result.failure<Unit>(RuntimeException("Failed to knock room $roomIdOrAlias"))
} }
val fakeKnockRoom = FakeKnockRoom(knockRoomSuccess) val fakeKnockRoom = FakeKnockRoom(knockRoomSuccess)
val presenter = createJoinRoomPresenter(knockRoom = fakeKnockRoom) val presenter = createJoinRoomPresenter(knockRoom = fakeKnockRoom)
presenter.test { presenter.test {
skipItems(1) skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.UpdateKnockMessage(knockMessage))
}
awaitItem().also { state -> awaitItem().also { state ->
state.eventSink(JoinRoomEvents.KnockRoom) state.eventSink(JoinRoomEvents.KnockRoom)
} }
@ -353,8 +361,46 @@ class JoinRoomPresenterTest {
} }
assert(knockRoomSuccess) assert(knockRoomSuccess)
.isCalledOnce() .isCalledOnce()
.with(value(A_ROOM_ID)) .with(value(A_ROOM_ID.toRoomIdOrAlias()), value(knockMessage), any())
assert(knockRoomFailure) assert(knockRoomFailure)
.isCalledOnce()
.with(value(A_ROOM_ID.toRoomIdOrAlias()), value(knockMessage), any())
}
@Test
fun `present - emit cancel knock room event`() = runTest {
val cancelKnockRoomSuccess = lambdaRecorder { _: RoomId ->
Result.success(Unit)
}
val cancelKnockRoomFailure = lambdaRecorder { roomId: RoomId ->
Result.failure<Unit>(RuntimeException("Failed to knock room $roomId"))
}
val cancelKnockRoom = FakeCancelKnockRoom(cancelKnockRoomSuccess)
val presenter = createJoinRoomPresenter(cancelKnockRoom = cancelKnockRoom)
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.CancelKnock(true))
}
awaitItem().also { state ->
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.ConfirmingNoParams)
state.eventSink(JoinRoomEvents.CancelKnock(false))
}
assertThat(awaitItem().cancelKnockAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.Success(Unit))
cancelKnockRoom.lambda = cancelKnockRoomFailure
state.eventSink(JoinRoomEvents.CancelKnock(false))
}
assertThat(awaitItem().cancelKnockAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
assertThat(state.cancelKnockAction).isInstanceOf(AsyncAction.Failure::class.java)
}
}
assert(cancelKnockRoomFailure)
.isCalledOnce()
.with(value(A_ROOM_ID))
assert(cancelKnockRoomSuccess)
.isCalledOnce() .isCalledOnce()
.with(value(A_ROOM_ID)) .with(value(A_ROOM_ID))
} }
@ -474,6 +520,7 @@ class JoinRoomPresenterTest {
Result.success(Unit) Result.success(Unit)
}, },
knockRoom: KnockRoom = FakeKnockRoom(), knockRoom: KnockRoom = FakeKnockRoom(),
cancelKnockRoom: CancelKnockRoom = FakeCancelKnockRoom(),
buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"), buildMeta: BuildMeta = aBuildMeta(applicationName = "AppName"),
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() } acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() }
): JoinRoomPresenter { ): JoinRoomPresenter {
@ -486,6 +533,7 @@ class JoinRoomPresenterTest {
matrixClient = matrixClient, matrixClient = matrixClient,
joinRoom = FakeJoinRoom(joinRoomLambda), joinRoom = FakeJoinRoom(joinRoomLambda),
knockRoom = knockRoom, knockRoom = knockRoom,
cancelKnockRoom = cancelKnockRoom,
buildMeta = buildMeta, buildMeta = buildMeta,
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter acceptDeclineInvitePresenter = acceptDeclineInvitePresenter
) )

View file

@ -61,6 +61,7 @@ class JoinRoomViewTest {
rule.setJoinRoomView( rule.setJoinRoomView(
aJoinRoomState( aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock), contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
knockMessage = "Knock knock",
eventSink = eventsRecorder, eventSink = eventsRecorder,
), ),
) )
@ -82,6 +83,33 @@ class JoinRoomViewTest {
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates) eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
} }
@Test
fun `clicking on cancel knock request emit the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked),
eventSink = eventsRecorder,
),
)
rule.clickOn(R.string.screen_join_room_cancel_knock_action)
eventsRecorder.assertSingle(JoinRoomEvents.CancelKnock(true))
}
@Test
fun `clicking on closing Cancel Knock error emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked),
cancelKnockAction = AsyncAction.Failure(Exception("Error")),
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
}
@Test @Test
fun `clicking on closing Join error emits the expected Event`() { fun `clicking on closing Join error emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
@ -170,6 +198,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinR
onBackClick: () -> Unit = EnsureNeverCalled(), onBackClick: () -> Unit = EnsureNeverCalled(),
onJoinSuccess: () -> Unit = EnsureNeverCalled(), onJoinSuccess: () -> Unit = EnsureNeverCalled(),
onKnockSuccess: () -> Unit = EnsureNeverCalled(), onKnockSuccess: () -> Unit = EnsureNeverCalled(),
onCancelKnockSuccess: () -> Unit = EnsureNeverCalled(),
) { ) {
setContent { setContent {
JoinRoomView( JoinRoomView(
@ -177,6 +206,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinR
onBackClick = onBackClick, onBackClick = onBackClick,
onJoinSuccess = onJoinSuccess, onJoinSuccess = onJoinSuccess,
onKnockSuccess = onKnockSuccess, onKnockSuccess = onKnockSuccess,
onCancelKnockSuccess = onCancelKnockSuccess
) )
} }
} }

View file

@ -21,9 +21,9 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.PendingRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
import io.element.android.libraries.matrix.api.room.PendingRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.preview.RoomPreview import io.element.android.libraries.matrix.api.room.preview.RoomPreview
@ -65,7 +65,7 @@ interface MatrixClient : Closeable {
suspend fun removeAvatar(): Result<Unit> suspend fun removeAvatar(): Result<Unit>
suspend fun joinRoom(roomId: RoomId): Result<RoomSummary?> suspend fun joinRoom(roomId: RoomId): Result<RoomSummary?>
suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomSummary?> suspend fun joinRoomByIdOrAlias(roomIdOrAlias: RoomIdOrAlias, serverNames: List<String>): Result<RoomSummary?>
suspend fun knockRoom(roomId: RoomId): Result<RoomSummary?> suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<RoomSummary?>
fun syncService(): SyncService fun syncService(): SyncService
fun sessionVerificationService(): SessionVerificationService fun sessionVerificationService(): SessionVerificationService
fun pushersService(): PushersService fun pushersService(): PushersService

View file

@ -30,8 +30,8 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.PushersService
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.PendingRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.PendingRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.preview.RoomPreview import io.element.android.libraries.matrix.api.room.preview.RoomPreview
@ -394,11 +394,13 @@ class RustMatrixClient(
} }
} }
override suspend fun knockRoom(roomId: RoomId): Result<RoomSummary?> = withContext(sessionDispatcher){ override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<RoomSummary?> = withContext(
sessionDispatcher
) {
runCatching { runCatching {
client.knock(roomId.toRoomIdOrAlias().identifier).destroy() client.knock(roomIdOrAlias.identifier).destroy()
try { try {
awaitRoom(roomId.toRoomIdOrAlias(), 10.seconds, CurrentUserMembership.KNOCKED) awaitRoom(roomIdOrAlias, 10.seconds, CurrentUserMembership.KNOCKED)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "Timeout waiting for the room to be available in the room list") Timber.e(e, "Timeout waiting for the room to be available in the room list")
null null

View file

@ -50,7 +50,6 @@ class RustRoomFactory(
private val roomSyncSubscriber: RoomSyncSubscriber, private val roomSyncSubscriber: RoomSyncSubscriber,
private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory, private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory,
) { ) {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private val dispatcher = dispatchers.io.limitedParallelism(1) private val dispatcher = dispatchers.io.limitedParallelism(1)
private val mutex = Mutex() private val mutex = Mutex()

View file

@ -22,8 +22,8 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.PendingRoom
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.PendingRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.preview.RoomPreview import io.element.android.libraries.matrix.api.room.preview.RoomPreview
@ -114,7 +114,7 @@ class FakeMatrixClient(
var joinRoomByIdOrAliasLambda: (RoomIdOrAlias, List<String>) -> Result<RoomSummary?> = { _, _ -> var joinRoomByIdOrAliasLambda: (RoomIdOrAlias, List<String>) -> Result<RoomSummary?> = { _, _ ->
Result.success(null) Result.success(null)
} }
var knockRoomLambda: (RoomId) -> Result<RoomSummary?> = { var knockRoomLambda: (RoomIdOrAlias, String, List<String>) -> Result<RoomSummary?> = { _, _, _ ->
Result.success(null) Result.success(null)
} }
var getRoomSummaryFlowLambda = { _: RoomIdOrAlias -> var getRoomSummaryFlowLambda = { _: RoomIdOrAlias ->
@ -223,7 +223,9 @@ class FakeMatrixClient(
return joinRoomByIdOrAliasLambda(roomIdOrAlias, serverNames) return joinRoomByIdOrAliasLambda(roomIdOrAlias, serverNames)
} }
override suspend fun knockRoom(roomId: RoomId): Result<RoomSummary?> = knockRoomLambda(roomId) override suspend fun knockRoom(roomIdOrAlias: RoomIdOrAlias, message: String, serverNames: List<String>): Result<RoomSummary?> {
return knockRoomLambda(roomIdOrAlias, message, serverNames)
}
override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService

View file

@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
@ -35,7 +34,7 @@ fun InviteSenderView(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier, modifier = modifier,
) { ) {
Box(modifier = Modifier.padding(vertical = 2.dp)){ Box(modifier = Modifier.padding(vertical = 2.dp)) {
Avatar(avatarData = inviteSender.avatarData) Avatar(avatarData = inviteSender.avatarData)
} }
Text( Text(