Merge pull request #4353 from element-hq/feature/fga/room_preview_invite_state

[Change] Invited state room preview
This commit is contained in:
ganfra 2025-03-04 16:45:50 +01:00 committed by GitHub
commit bc8241f19c
28 changed files with 327 additions and 144 deletions

View file

@ -8,6 +8,6 @@
package io.element.android.features.invite.api.response package io.element.android.features.invite.api.response
interface AcceptDeclineInviteEvents { interface AcceptDeclineInviteEvents {
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents data class AcceptInvite(val invite: InviteData?) : AcceptDeclineInviteEvents
data class DeclineInvite(val invite: InviteData) : AcceptDeclineInviteEvents data class DeclineInvite(val invite: InviteData?, val blockUser: Boolean = false) : AcceptDeclineInviteEvents
} }

View file

@ -10,6 +10,7 @@ package io.element.android.features.invite.api.response
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
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.UserId
open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDeclineInviteState> { open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDeclineInviteState> {
override val values: Sequence<AcceptDeclineInviteState> override val values: Sequence<AcceptDeclineInviteState>
@ -17,12 +18,20 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDec
anAcceptDeclineInviteState(), anAcceptDeclineInviteState(),
anAcceptDeclineInviteState( anAcceptDeclineInviteState(
declineAction = ConfirmingDeclineInvite( declineAction = ConfirmingDeclineInvite(
InviteData(RoomId("!room:matrix.org"), isDm = true, roomName = "Alice") InviteData(roomId = RoomId("!room:matrix.org"), isDm = true, roomName = "Alice", senderId = UserId("@alice:matrix.org")),
blockUser = false,
), ),
), ),
anAcceptDeclineInviteState( anAcceptDeclineInviteState(
declineAction = ConfirmingDeclineInvite( declineAction = ConfirmingDeclineInvite(
InviteData(RoomId("!room:matrix.org"), isDm = false, roomName = "Some room") InviteData(roomId = RoomId("!room:matrix.org"), isDm = false, roomName = "Some room", senderId = UserId("@alice:matrix.org")),
blockUser = false,
),
),
anAcceptDeclineInviteState(
declineAction = ConfirmingDeclineInvite(
InviteData(roomId = RoomId("!room:matrix.org"), isDm = true, roomName = "Alice", senderId = UserId("@alice:matrix.org")),
blockUser = true,
), ),
), ),
anAcceptDeclineInviteState( anAcceptDeclineInviteState(

View file

@ -11,4 +11,5 @@ import io.element.android.libraries.architecture.AsyncAction
data class ConfirmingDeclineInvite( data class ConfirmingDeclineInvite(
val inviteData: InviteData, val inviteData: InviteData,
val blockUser: Boolean,
) : AsyncAction.Confirming ) : AsyncAction.Confirming

View file

@ -8,8 +8,10 @@
package io.element.android.features.invite.api.response package io.element.android.features.invite.api.response
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.UserId
data class InviteData( data class InviteData(
val senderId: UserId,
val roomId: RoomId, val roomId: RoomId,
val roomName: String, val roomName: String,
val isDm: Boolean, val isDm: Boolean,

View file

@ -16,6 +16,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.ConfirmingDeclineInvite import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
import io.element.android.features.invite.api.response.InviteData
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
@ -43,15 +44,34 @@ class AcceptDeclineInvitePresenter @Inject constructor(
fun handleEvents(event: AcceptDeclineInviteEvents) { fun handleEvents(event: AcceptDeclineInviteEvents) {
when (event) { when (event) {
is AcceptDeclineInviteEvents.AcceptInvite -> { is AcceptDeclineInviteEvents.AcceptInvite -> {
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction) val inviteData = event.invite
if (inviteData == null) {
acceptedAction.value = AsyncAction.Failure(InvalidDataException())
} else {
localCoroutineScope.acceptInvite(inviteData.roomId, acceptedAction)
}
} }
is AcceptDeclineInviteEvents.DeclineInvite -> { is AcceptDeclineInviteEvents.DeclineInvite -> {
declinedAction.value = ConfirmingDeclineInvite(event.invite) val inviteData = event.invite
if (inviteData == null) {
declinedAction.value = AsyncAction.Failure(InvalidDataException())
} else {
declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser)
}
} }
is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> { is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
localCoroutineScope.declineInvite(event.roomId, declinedAction) when (val declinedActionValue = declinedAction.value) {
is ConfirmingDeclineInvite -> {
localCoroutineScope.declineInvite(
inviteData = declinedActionValue.inviteData,
declinedAction = declinedAction,
blockUser = declinedActionValue.blockUser,
)
}
else -> Unit
}
} }
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> { is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
@ -92,13 +112,20 @@ class AcceptDeclineInvitePresenter @Inject constructor(
} }
} }
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<AsyncAction<RoomId>>) = launch { private fun CoroutineScope.declineInvite(
inviteData: InviteData,
blockUser: Boolean,
declinedAction: MutableState<AsyncAction<RoomId>>,
) = launch {
suspend { suspend {
client.getPendingRoom(roomId)?.use { client.getPendingRoom(inviteData.roomId)?.use {
it.leave().getOrThrow() it.leave().getOrThrow()
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
} }
roomId if (blockUser) {
client.ignoreUser(inviteData.senderId).getOrThrow()
}
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, inviteData.roomId)
inviteData.roomId
}.runCatchingUpdatingState(declinedAction) }.runCatchingUpdatingState(declinedAction)
} }
} }

View file

@ -50,8 +50,9 @@ fun AcceptDeclineInviteView(
if (confirming is ConfirmingDeclineInvite) { if (confirming is ConfirmingDeclineInvite) {
DeclineConfirmationDialog( DeclineConfirmationDialog(
invite = confirming.inviteData, invite = confirming.inviteData,
blockUser = confirming.blockUser,
onConfirmClick = { onConfirmClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(confirming.inviteData.roomId)) state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite)
}, },
onDismissClick = { onDismissClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite) state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)
@ -66,29 +67,35 @@ fun AcceptDeclineInviteView(
@Composable @Composable
private fun DeclineConfirmationDialog( private fun DeclineConfirmationDialog(
invite: InviteData, invite: InviteData,
blockUser: Boolean,
onConfirmClick: () -> Unit, onConfirmClick: () -> Unit,
onDismissClick: () -> Unit, onDismissClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val contentResource = if (invite.isDm) { val senderId = invite.senderId.value
R.string.screen_invites_decline_direct_chat_message val content = when {
} else { blockUser -> stringResource(R.string.screen_join_room_decline_and_block_alert_message, senderId)
R.string.screen_invites_decline_chat_message invite.isDm -> stringResource(R.string.screen_invites_decline_direct_chat_message, invite.roomName)
else -> stringResource(R.string.screen_invites_decline_chat_message, invite.roomName)
} }
val title = when {
val titleResource = if (invite.isDm) { blockUser -> stringResource(R.string.screen_join_room_decline_and_block_alert_title)
R.string.screen_invites_decline_direct_chat_title invite.isDm -> stringResource(R.string.screen_invites_decline_direct_chat_title)
} else { else -> stringResource(R.string.screen_invites_decline_chat_title)
R.string.screen_invites_decline_chat_title }
val submitText = if (blockUser) {
stringResource(R.string.screen_join_room_decline_and_block_alert_confirmation)
} else {
stringResource(CommonStrings.action_decline)
} }
ConfirmationDialog( ConfirmationDialog(
modifier = modifier, modifier = modifier,
content = stringResource(contentResource, invite.roomName), content = content,
title = stringResource(titleResource), title = title,
submitText = stringResource(CommonStrings.action_decline), submitText = submitText,
cancelText = stringResource(CommonStrings.action_cancel), cancelText = stringResource(CommonStrings.action_cancel),
onSubmitClick = onConfirmClick, onSubmitClick = onConfirmClick,
destructiveSubmit = blockUser,
onDismiss = onDismissClick, onDismiss = onDismissClick,
) )
} }

View file

@ -8,10 +8,9 @@
package io.element.android.features.invite.impl.response package io.element.android.features.invite.impl.response
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
import io.element.android.libraries.matrix.api.core.RoomId
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents { sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
data class ConfirmDeclineInvite(val roomId: RoomId) : InternalAcceptDeclineInviteEvents data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents
data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
data object DismissAcceptError : InternalAcceptDeclineInviteEvents data object DismissAcceptError : InternalAcceptDeclineInviteEvents
data object DismissDeclineError : InternalAcceptDeclineInviteEvents data object DismissDeclineError : InternalAcceptDeclineInviteEvents

View file

@ -0,0 +1,10 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.invite.impl.response
class InvalidDataException : Exception()

View file

@ -6,4 +6,8 @@
<string name="screen_invites_decline_direct_chat_title">"Decline chat"</string> <string name="screen_invites_decline_direct_chat_title">"Decline chat"</string>
<string name="screen_invites_empty_list">"No Invites"</string> <string name="screen_invites_empty_list">"No Invites"</string>
<string name="screen_invites_invited_you">"%1$s (%2$s) invited you"</string> <string name="screen_invites_invited_you">"%1$s (%2$s) invited you"</string>
<string name="screen_join_room_decline_and_block_alert_confirmation">"Yes, decline &amp; block"</string>
<string name="screen_join_room_decline_and_block_alert_message">"Are you sure you want to decline the invite to join this room? This will also prevent %1$s from contacting you or inviting you to rooms."</string>
<string name="screen_join_room_decline_and_block_alert_title">"Decline invite &amp; block"</string>
<string name="screen_join_room_decline_and_block_button_title">"Decline and block"</string>
</resources> </resources>

View file

@ -17,10 +17,12 @@ 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.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.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
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
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeRoomPreview import io.element.android.libraries.matrix.test.room.FakeRoomPreview
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
@ -61,7 +63,7 @@ class AcceptDeclineInvitePresenterTest {
) )
} }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData)) assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink( state.eventSink(
InternalAcceptDeclineInviteEvents.CancelDeclineInvite InternalAcceptDeclineInviteEvents.CancelDeclineInvite
) )
@ -91,9 +93,9 @@ class AcceptDeclineInvitePresenterTest {
) )
} }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData)) assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink( state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId) InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
) )
} }
assertThat(awaitItem().declineAction.isLoading()).isTrue() assertThat(awaitItem().declineAction.isLoading()).isTrue()
@ -139,9 +141,9 @@ class AcceptDeclineInvitePresenterTest {
) )
} }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData)) assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink( state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite(inviteData.roomId) InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
) )
} }
assertThat(awaitItem().declineAction.isLoading()).isTrue() assertThat(awaitItem().declineAction.isLoading()).isTrue()
@ -156,6 +158,80 @@ class AcceptDeclineInvitePresenterTest {
.with(value(A_SESSION_ID), value(A_ROOM_ID)) .with(value(A_SESSION_ID), value(A_ROOM_ID))
} }
@Test
fun `present - declining invite with block success flow`() = runTest {
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
Result.success(Unit)
}
val fakeNotificationCleaner = FakeNotificationCleaner(
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
)
val declineInviteSuccess = lambdaRecorder { -> Result.success(Unit) }
val ignoreUserSuccess = lambdaRecorder { _: UserId -> Result.success(Unit) }
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
Result.success(FakeRoomPreview(declineInviteResult = declineInviteSuccess))
},
ignoreUserResult = ignoreUserSuccess
)
val presenter = createAcceptDeclineInvitePresenter(
client = client,
notificationCleaner = fakeNotificationCleaner,
)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, true))
state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
}
cancelAndConsumeRemainingEvents()
}
declineInviteSuccess.assertions().isCalledOnce()
ignoreUserSuccess.assertions().isCalledOnce().with(value(A_USER_ID))
clearMembershipNotificationForRoomLambda.assertions()
.isCalledOnce()
.with(value(A_SESSION_ID), value(A_ROOM_ID))
}
@Test
fun `present - declining invite with block error flow`() = runTest {
val declineInviteFailure = lambdaRecorder { ->
Result.failure<Unit>(RuntimeException("Failed to leave room"))
}
val client = FakeMatrixClient(
getRoomPreviewResult = { _, _ ->
Result.success(FakeRoomPreview(declineInviteResult = declineInviteFailure))
}
)
val presenter = createAcceptDeclineInvitePresenter(client = client)
presenter.test {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, true))
state.eventSink(
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
}
}
@Test @Test
fun `present - accepting invite error flow`() = runTest { fun `present - accepting invite error flow`() = runTest {
val joinRoomFailure = lambdaRecorder { roomIdOrAlias: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger -> val joinRoomFailure = lambdaRecorder { roomIdOrAlias: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
@ -237,12 +313,14 @@ class AcceptDeclineInvitePresenterTest {
private fun anInviteData( private fun anInviteData(
roomId: RoomId = A_ROOM_ID, roomId: RoomId = A_ROOM_ID,
name: String = A_ROOM_NAME, name: String = A_ROOM_NAME,
isDm: Boolean = false isDm: Boolean = false,
senderId: UserId = A_USER_ID,
): InviteData { ): InviteData {
return InviteData( return InviteData(
roomId = roomId, roomId = roomId,
roomName = name, roomName = name,
isDm = isDm isDm = isDm,
senderId = senderId,
) )
} }

View file

@ -17,5 +17,5 @@ sealed interface JoinRoomEvents {
data class UpdateKnockMessage(val message: String) : 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 class DeclineInvite(val blockUser: Boolean) : JoinRoomEvents
} }

View file

@ -152,15 +152,15 @@ class JoinRoomPresenter @AssistedInject constructor(
JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction) JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage) is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
JoinRoomEvents.AcceptInvite -> { JoinRoomEvents.AcceptInvite -> {
val inviteData = contentState.toInviteData() ?: return val inviteData = contentState.toInviteData()
acceptDeclineInviteState.eventSink( acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData) AcceptDeclineInviteEvents.AcceptInvite(inviteData)
) )
} }
JoinRoomEvents.DeclineInvite -> { is JoinRoomEvents.DeclineInvite -> {
val inviteData = contentState.toInviteData() ?: return val inviteData = contentState.toInviteData()
acceptDeclineInviteState.eventSink( acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData) AcceptDeclineInviteEvents.DeclineInvite(invite = inviteData, blockUser = event.blockUser)
) )
} }
is JoinRoomEvents.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction) is JoinRoomEvents.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction)
@ -314,12 +314,19 @@ private fun JoinRule?.toJoinAuthorisationStatus(): JoinAuthorisationStatus {
@VisibleForTesting @VisibleForTesting
internal fun ContentState.toInviteData(): InviteData? { internal fun ContentState.toInviteData(): InviteData? {
return when (this) { return when (this) {
is ContentState.Loaded -> InviteData( is ContentState.Loaded -> {
roomId = roomId, if (joinAuthorisationStatus is JoinAuthorisationStatus.IsInvited && joinAuthorisationStatus.inviteSender != null) {
// Note: name should not be null at this point, but use Id just in case... InviteData(
roomName = name ?: roomId.value, roomId = roomId,
isDm = isDm // Note: name should not be null at this point, but use Id just in case...
) roomName = name ?: roomId.value,
senderId = joinAuthorisationStatus.inviteSender.userId,
isDm = isDm
)
} else {
null
}
}
else -> null else -> null
} }
} }

View file

@ -63,6 +63,7 @@ import io.element.android.libraries.designsystem.theme.components.ButtonSize
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.OutlinedButton import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
@ -105,8 +106,8 @@ fun JoinRoomView(
onAcceptInvite = { onAcceptInvite = {
state.eventSink(JoinRoomEvents.AcceptInvite) state.eventSink(JoinRoomEvents.AcceptInvite)
}, },
onDeclineInvite = { onDeclineInvite = { blockUser ->
state.eventSink(JoinRoomEvents.DeclineInvite) state.eventSink(JoinRoomEvents.DeclineInvite(blockUser))
}, },
onJoinRoom = { onJoinRoom = {
state.eventSink(JoinRoomEvents.JoinRoom) state.eventSink(JoinRoomEvents.JoinRoom)
@ -183,7 +184,7 @@ fun JoinRoomView(
private fun JoinRoomFooter( private fun JoinRoomFooter(
joinAuthorisationStatus: JoinAuthorisationStatus, joinAuthorisationStatus: JoinAuthorisationStatus,
onAcceptInvite: () -> Unit, onAcceptInvite: () -> Unit,
onDeclineInvite: () -> Unit, onDeclineInvite: (Boolean) -> Unit,
onJoinRoom: () -> Unit, onJoinRoom: () -> Unit,
onKnockRoom: () -> Unit, onKnockRoom: () -> Unit,
onCancelKnock: () -> Unit, onCancelKnock: () -> Unit,
@ -193,23 +194,32 @@ private fun JoinRoomFooter(
) { ) {
Box( Box(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp) .padding(top = 8.dp)
) { ) {
when (joinAuthorisationStatus) { when (joinAuthorisationStatus) {
is JoinAuthorisationStatus.IsInvited -> { is JoinAuthorisationStatus.IsInvited -> {
ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) { Column {
OutlinedButton( ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
text = stringResource(CommonStrings.action_decline), OutlinedButton(
onClick = onDeclineInvite, text = stringResource(CommonStrings.action_decline),
modifier = Modifier.weight(1f), onClick = { onDeclineInvite(false) },
size = ButtonSize.LargeLowPadding, modifier = Modifier.weight(1f),
) size = ButtonSize.LargeLowPadding,
Button( )
text = stringResource(CommonStrings.action_accept), Button(
onClick = onAcceptInvite, text = stringResource(CommonStrings.action_accept),
modifier = Modifier.weight(1f), onClick = onAcceptInvite,
size = ButtonSize.LargeLowPadding, modifier = Modifier.weight(1f),
size = ButtonSize.LargeLowPadding,
)
}
Spacer(modifier = Modifier.height(24.dp))
TextButton(
text = stringResource(R.string.screen_join_room_decline_and_block_button_title),
onClick = { onDeclineInvite(true) },
modifier = Modifier.fillMaxWidth(),
destructive = true
) )
} }
} }
@ -372,12 +382,19 @@ private fun JoinRoomContent(
IsKnockedLoadedContent() IsKnockedLoadedContent()
} }
else -> { else -> {
DefaultLoadedContent( Column(horizontalAlignment = Alignment.CenterHorizontally) {
modifier = Modifier.verticalScroll(rememberScrollState()), val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
contentState = contentState, if (inviteSender != null) {
knockMessage = knockMessage, InviteSenderView(inviteSender = inviteSender)
onKnockMessageUpdate = onKnockMessageUpdate Spacer(modifier = Modifier.height(32.dp))
) }
DefaultLoadedContent(
modifier = Modifier.verticalScroll(rememberScrollState()),
contentState = contentState,
knockMessage = knockMessage,
onKnockMessageUpdate = onKnockMessageUpdate
)
}
} }
} }
} }
@ -440,8 +457,8 @@ private fun IncompleteContent(
private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) { private fun IsKnockedLoadedContent(modifier: Modifier = Modifier) {
BoxWithConstraints( BoxWithConstraints(
modifier = modifier modifier = modifier
.fillMaxHeight() .fillMaxHeight()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
IconTitleSubtitleMolecule( IconTitleSubtitleMolecule(
@ -487,10 +504,6 @@ private fun DefaultLoadedContent(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
val inviteSender = (contentState.joinAuthorisationStatus as? JoinAuthorisationStatus.IsInvited)?.inviteSender
if (inviteSender != null) {
InviteSenderView(inviteSender = inviteSender)
}
RoomPreviewDescriptionAtom(contentState.topic ?: "") RoomPreviewDescriptionAtom(contentState.topic ?: "")
if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) { if (contentState.joinAuthorisationStatus is JoinAuthorisationStatus.CanKnock) {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))

View file

@ -167,9 +167,9 @@ class JoinRoomPresenterTest {
awaitItem().also { state -> awaitItem().also { state ->
state.eventSink(JoinRoomEvents.AcceptInvite) state.eventSink(JoinRoomEvents.AcceptInvite)
state.eventSink(JoinRoomEvents.DeclineInvite) state.eventSink(JoinRoomEvents.DeclineInvite(false))
val inviteData = state.contentState.toInviteData()!! val inviteData = state.contentState.toInviteData()
assert(eventSinkRecorder) assert(eventSinkRecorder)
.isCalledExactly(2) .isCalledExactly(2)

View file

@ -139,7 +139,7 @@ class JoinRoomViewTest {
} }
@Test @Test
fun `clicking on Accept invitation IsInvited room emits the expected Event`() { fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView( rule.setJoinRoomView(
aJoinRoomState( aJoinRoomState(
@ -152,7 +152,7 @@ class JoinRoomViewTest {
} }
@Test @Test
fun `clicking on Decline invitation on IsInvited room emits the expected Event`() { fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>() val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView( rule.setJoinRoomView(
aJoinRoomState( aJoinRoomState(
@ -161,7 +161,20 @@ class JoinRoomViewTest {
), ),
) )
rule.clickOn(CommonStrings.action_decline) rule.clickOn(CommonStrings.action_decline)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite) eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(false))
}
@Test
fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited emits the expected Event`() {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
rule.setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)),
eventSink = eventsRecorder,
),
)
rule.clickOn(R.string.screen_join_room_decline_and_block_button_title)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(true))
} }
@Test @Test

View file

@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -40,9 +41,9 @@ class BlockedUsersPresenterTest {
@Test @Test
fun `present - initial state with blocked users`() = runTest { fun `present - initial state with blocked users`() = runTest {
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient(
ignoredUsersFlow.value = persistentListOf(A_USER_ID) ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
} )
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -56,9 +57,10 @@ class BlockedUsersPresenterTest {
@Test @Test
fun `present - blocked users list updates with new emissions`() = runTest { fun `present - blocked users list updates with new emissions`() = runTest {
val matrixClient = FakeMatrixClient().apply { val ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
ignoredUsersFlow.value = persistentListOf(A_USER_ID) val matrixClient = FakeMatrixClient(
} ignoredUsersFlow = ignoredUsersFlow
)
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -66,7 +68,7 @@ class BlockedUsersPresenterTest {
with(awaitItem()) { with(awaitItem()) {
assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID))) assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID)))
} }
matrixClient.ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2) ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2)
skipItems(1) skipItems(1)
with(awaitItem()) { with(awaitItem()) {
assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID), MatrixUser(A_USER_ID_2))) assertThat(blockedUsers).isEqualTo(listOf(MatrixUser(A_USER_ID), MatrixUser(A_USER_ID_2)))
@ -77,8 +79,9 @@ class BlockedUsersPresenterTest {
@Test @Test
fun `present - blocked users list with data`() = runTest { fun `present - blocked users list with data`() = runTest {
val alice = MatrixUser(A_USER_ID, displayName = "Alice", avatarUrl = "aliceAvatar") val alice = MatrixUser(A_USER_ID, displayName = "Alice", avatarUrl = "aliceAvatar")
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient(
ignoredUsersFlow.value = persistentListOf(A_USER_ID, A_USER_ID_2) ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID, A_USER_ID_2))
).apply {
givenGetProfileResult(A_USER_ID, Result.success(alice)) givenGetProfileResult(A_USER_ID, Result.success(alice))
givenGetProfileResult(A_USER_ID_2, Result.failure(AN_EXCEPTION)) givenGetProfileResult(A_USER_ID_2, Result.failure(AN_EXCEPTION))
} }
@ -103,9 +106,9 @@ class BlockedUsersPresenterTest {
@Test @Test
fun `present - unblock user`() = runTest { fun `present - unblock user`() = runTest {
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient(
ignoredUsersFlow.value = persistentListOf(A_USER_ID) ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
} )
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -125,10 +128,10 @@ class BlockedUsersPresenterTest {
@Test @Test
fun `present - unblock user handles failure`() = runTest { fun `present - unblock user handles failure`() = runTest {
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient(
ignoredUsersFlow.value = persistentListOf(A_USER_ID) unIgnoreUserResult = { Result.failure(IllegalStateException("User not banned")) },
givenUnignoreUserResult(Result.failure(IllegalStateException("User not banned"))) ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
} )
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -147,10 +150,10 @@ class BlockedUsersPresenterTest {
@Test @Test
fun `present - unblock user then cancel`() = runTest { fun `present - unblock user then cancel`() = runTest {
val matrixClient = FakeMatrixClient().apply { val matrixClient = FakeMatrixClient(
ignoredUsersFlow.value = persistentListOf(A_USER_ID) unIgnoreUserResult = { Result.failure(IllegalStateException("User not banned")) },
givenUnignoreUserResult(Result.failure(IllegalStateException("User not banned"))) ignoredUsersFlow = MutableStateFlow(persistentListOf(A_USER_ID))
} )
val presenter = aBlockedUsersPresenter(matrixClient = matrixClient) val presenter = aBlockedUsersPresenter(matrixClient = matrixClient)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()

View file

@ -329,9 +329,12 @@ class RoomListPresenter @Inject constructor(
} }
@VisibleForTesting @VisibleForTesting
internal fun RoomListRoomSummary.toInviteData() = InviteData( internal fun RoomListRoomSummary.toInviteData(): InviteData? {
roomId = roomId, if (inviteSender == null) return null
// Note: `name` should not be null at this point, but just in case, fallback to the roomId return InviteData(
roomName = name ?: roomId.value, roomId = roomId,
isDm = isDm, roomName = name ?: roomId.value,
) isDm = isDm,
senderId = inviteSender.userId,
)
}

View file

@ -40,7 +40,11 @@ import io.element.android.tests.testutils.lambda.any
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
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -169,7 +173,8 @@ class UserProfilePresenterTest {
@Test @Test
fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest { fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest {
val client = createFakeMatrixClient() val ignoredUsersFlow = MutableStateFlow(persistentListOf<UserId>())
val client = createFakeMatrixClient(ignoredUsersFlow = ignoredUsersFlow)
val presenter = createUserProfilePresenter( val presenter = createUserProfilePresenter(
client = client, client = client,
userId = A_USER_ID userId = A_USER_ID
@ -178,20 +183,21 @@ class UserProfilePresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false)) initialState.eventSink(UserProfileEvents.BlockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue() assertThat(awaitItem().isBlocked.isLoading()).isTrue()
client.emitIgnoreUserList(listOf(A_USER_ID)) ignoredUsersFlow.emit(persistentListOf(A_USER_ID))
assertThat(awaitItem().isBlocked.dataOrNull()).isTrue() assertThat(awaitItem().isBlocked.dataOrNull()).isTrue()
initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false)) initialState.eventSink(UserProfileEvents.UnblockUser(needsConfirmation = false))
assertThat(awaitItem().isBlocked.isLoading()).isTrue() assertThat(awaitItem().isBlocked.isLoading()).isTrue()
client.emitIgnoreUserList(listOf()) ignoredUsersFlow.emit(persistentListOf())
assertThat(awaitItem().isBlocked.dataOrNull()).isFalse() assertThat(awaitItem().isBlocked.dataOrNull()).isFalse()
} }
} }
@Test @Test
fun `present - BlockUser with error`() = runTest { fun `present - BlockUser with error`() = runTest {
val matrixClient = createFakeMatrixClient() val matrixClient = createFakeMatrixClient(
matrixClient.givenIgnoreUserResult(Result.failure(A_THROWABLE)) ignoreUserResult = { Result.failure(A_THROWABLE) }
)
val presenter = createUserProfilePresenter(client = matrixClient) val presenter = createUserProfilePresenter(client = matrixClient)
presenter.test { presenter.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
@ -207,8 +213,9 @@ class UserProfilePresenterTest {
@Test @Test
fun `present - UnblockUser with error`() = runTest { fun `present - UnblockUser with error`() = runTest {
val matrixClient = createFakeMatrixClient() val matrixClient = createFakeMatrixClient(
matrixClient.givenUnignoreUserResult(Result.failure(A_THROWABLE)) unIgnoreUserResult = { Result.failure(A_THROWABLE) }
)
val presenter = createUserProfilePresenter(client = matrixClient) val presenter = createUserProfilePresenter(client = matrixClient)
presenter.test { presenter.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
@ -374,10 +381,16 @@ class UserProfilePresenterTest {
private fun createFakeMatrixClient( private fun createFakeMatrixClient(
isUserVerified: Boolean = false, isUserVerified: Boolean = false,
ignoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
unIgnoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
ignoredUsersFlow: StateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf())
) = FakeMatrixClient( ) = FakeMatrixClient(
encryptionService = FakeEncryptionService( encryptionService = FakeEncryptionService(
isUserVerifiedResult = { Result.success(isUserVerified) } isUserVerifiedResult = { Result.success(isUserVerified) }
), ),
ignoreUserResult = ignoreUserResult,
unIgnoreUserResult = unIgnoreUserResult,
ignoredUsersFlow = ignoredUsersFlow
) )
private fun createUserProfilePresenter( private fun createUserProfilePresenter(

View file

@ -47,7 +47,6 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -85,7 +84,10 @@ class FakeMatrixClient(
private val canDeactivateAccountResult: () -> Boolean = { lambdaError() }, private val canDeactivateAccountResult: () -> Boolean = { lambdaError() },
private val deactivateAccountResult: (String, Boolean) -> Result<Unit> = { _, _ -> lambdaError() }, private val deactivateAccountResult: (String, Boolean) -> Result<Unit> = { _, _ -> lambdaError() },
private val currentSlidingSyncVersionLambda: () -> Result<SlidingSyncVersion> = { lambdaError() }, private val currentSlidingSyncVersionLambda: () -> Result<SlidingSyncVersion> = { lambdaError() },
private val availableSlidingSyncVersionsLambda: () -> Result<List<SlidingSyncVersion>> = { lambdaError() } private val availableSlidingSyncVersionsLambda: () -> Result<List<SlidingSyncVersion>> = { lambdaError() },
private val ignoreUserResult: (UserId) -> Result<Unit> = { lambdaError() },
private var unIgnoreUserResult: (UserId) -> Result<Unit> = { Result.success(Unit) },
override val ignoredUsersFlow: StateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf()),
) : MatrixClient { ) : MatrixClient {
var setDisplayNameCalled: Boolean = false var setDisplayNameCalled: Boolean = false
private set private set
@ -96,10 +98,7 @@ class FakeMatrixClient(
private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(MatrixUser(sessionId, userDisplayName, userAvatarUrl)) private val _userProfile: MutableStateFlow<MatrixUser> = MutableStateFlow(MatrixUser(sessionId, userDisplayName, userAvatarUrl))
override val userProfile: StateFlow<MatrixUser> = _userProfile override val userProfile: StateFlow<MatrixUser> = _userProfile
override val ignoredUsersFlow: MutableStateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf())
private var ignoreUserResult: Result<Unit> = Result.success(Unit)
private var unignoreUserResult: Result<Unit> = Result.success(Unit)
private var createRoomResult: Result<RoomId> = Result.success(A_ROOM_ID) private var createRoomResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID) private var createDmResult: Result<RoomId> = Result.success(A_ROOM_ID)
private var findDmResult: RoomId? = A_ROOM_ID private var findDmResult: RoomId? = A_ROOM_ID
@ -137,11 +136,11 @@ class FakeMatrixClient(
} }
override suspend fun ignoreUser(userId: UserId): Result<Unit> = simulateLongTask { override suspend fun ignoreUser(userId: UserId): Result<Unit> = simulateLongTask {
return ignoreUserResult return ignoreUserResult(userId)
} }
override suspend fun unignoreUser(userId: UserId): Result<Unit> = simulateLongTask { override suspend fun unignoreUser(userId: UserId): Result<Unit> = simulateLongTask {
return unignoreUserResult return unIgnoreUserResult(userId)
} }
override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> = simulateLongTask { override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> = simulateLongTask {
@ -239,10 +238,6 @@ class FakeMatrixClient(
return RoomMembershipObserver() return RoomMembershipObserver()
} }
suspend fun emitIgnoreUserList(users: List<UserId>) {
ignoredUsersFlow.emit(users.toImmutableList())
}
// Mocks // Mocks
fun givenCreateRoomResult(result: Result<RoomId>) { fun givenCreateRoomResult(result: Result<RoomId>) {
@ -253,14 +248,6 @@ class FakeMatrixClient(
createDmResult = result createDmResult = result
} }
fun givenIgnoreUserResult(result: Result<Unit>) {
ignoreUserResult = result
}
fun givenUnignoreUserResult(result: Result<Unit>) {
unignoreUserResult = result
}
fun givenFindDmResult(result: RoomId?) { fun givenFindDmResult(result: RoomId?) {
findDmResult = result findDmResult = result
} }

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:3f5e0ac8b492e4152a0eb8ef9cb0b6456b6cd9dad9733a23c99ad53def2d15ff oid sha256:970efb76758b76905d04d159e123201bbcf34e25ca92ba49956f5ed4beb68d1e
size 9962 size 37097

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3f5e0ac8b492e4152a0eb8ef9cb0b6456b6cd9dad9733a23c99ad53def2d15ff
size 9962

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9e05f1b1cfc75e0698e82c76985255d43838fc9e07dbfda39a5f7b27dcd7dcc8 oid sha256:6b07a822f818e4709be8afe2e3a79a8aa03b42bb67ec3206cc310b5386037ab3
size 8596 size 34758

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9e05f1b1cfc75e0698e82c76985255d43838fc9e07dbfda39a5f7b27dcd7dcc8
size 8596

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9a869730be3df8fdcbaa2bd251968b330a5e82733cfd6efa86e8a4971552a306 oid sha256:2770e628229279569dd66e62395cce39e763ffdf64ddd9901e72afa3c7055c07
size 26612 size 29911

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e97676e6f90749d608c8289137c39668c122228f6660af9ed4b056f4f05d61ca oid sha256:8286e517a48da892830571538a9047d9d91daae01400ce0ff19673a932debf96
size 34946 size 37727

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7b16b479b55dcdc22e17be7d9a0996de4b7f1c0c708a013c134483369073b316 oid sha256:25fc6358031c6f3b10401b2e8e5227ae0c63b36bd77e0d2f115eb19988f8cfbd
size 26237 size 29242

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d4a30364d8f94b77b7f518107c531f0fe2e1e9e16729777e8d84958e06b198af oid sha256:58178d173a6575ea5bdfce718f5d1686767f7078e19df2c18b55dca7ff6edb2f
size 34270 size 36944

View file

@ -56,7 +56,8 @@
{ {
"name" : ":features:invite:impl", "name" : ":features:invite:impl",
"includeRegex" : [ "includeRegex" : [
"screen_invites_.*" "screen_invites_.*",
"screen\\.join_room\\.decline_and_block_.*"
] ]
}, },
{ {