Accepting and declining invites
Hook up accept and decline buttons in the invites UI. Accept will attempt to accept and then navigate to the room; decline shows a confirmation dialog. Fixes #106
This commit is contained in:
@@ -227,6 +227,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
override fun onBackClicked() {
|
||||
backstack.pop()
|
||||
}
|
||||
|
||||
override fun onInviteAccepted(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId))
|
||||
}
|
||||
}
|
||||
|
||||
inviteListEntryPoint.nodeBuilder(this, buildContext)
|
||||
|
||||
1
changelog.d/106.feature
Normal file
1
changelog.d/106.feature
Normal file
@@ -0,0 +1 @@
|
||||
[Create and join rooms] Accept or decline an invite from invitation list
|
||||
@@ -24,4 +24,5 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
interface InviteListEntryPoint : FeatureEntryPoint {
|
||||
|
||||
@@ -32,6 +33,8 @@ interface InviteListEntryPoint : FeatureEntryPoint {
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onBackClicked()
|
||||
|
||||
fun onInviteAccepted(roomId: RoomId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.invitelist.impl
|
||||
|
||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||
|
||||
sealed interface InviteListEvents {
|
||||
|
||||
data class AcceptInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||
data class DeclineInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||
|
||||
object ConfirmDeclineInvite: InviteListEvents
|
||||
object CancelDeclineInvite: InviteListEvents
|
||||
|
||||
object DismissAcceptError: InviteListEvents
|
||||
object DismissDeclineError: InviteListEvents
|
||||
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.invitelist.api.InviteListEntryPoint
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class InviteListNode @AssistedInject constructor(
|
||||
@@ -39,12 +40,17 @@ class InviteListNode @AssistedInject constructor(
|
||||
plugins<InviteListEntryPoint.Callback>().forEach { it.onBackClicked() }
|
||||
}
|
||||
|
||||
private fun onInviteAccepted(roomId: RoomId) {
|
||||
plugins<InviteListEntryPoint.Callback>().forEach { it.onInviteAccepted(roomId) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
InviteListView(
|
||||
state = state,
|
||||
onBackClicked = ::onBackClicked,
|
||||
onInviteAccepted = ::onInviteAccepted,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,15 +17,24 @@
|
||||
package io.element.android.features.invitelist.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.architecture.execute
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomSummary
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class InviteListPresenter @Inject constructor(
|
||||
@@ -39,11 +48,71 @@ class InviteListPresenter @Inject constructor(
|
||||
.roomSummaries()
|
||||
.collectAsState()
|
||||
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val acceptedAction: MutableState<Async<RoomId>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
val declinedAction: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) }
|
||||
val decliningInvite: MutableState<InviteListInviteSummary?> = remember { mutableStateOf(null) }
|
||||
|
||||
fun handleEvent(event: InviteListEvents) {
|
||||
when (event) {
|
||||
is InviteListEvents.AcceptInvite -> {
|
||||
acceptedAction.value = Async.Uninitialized
|
||||
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
|
||||
}
|
||||
|
||||
is InviteListEvents.DeclineInvite -> {
|
||||
decliningInvite.value = event.invite
|
||||
}
|
||||
|
||||
is InviteListEvents.ConfirmDeclineInvite -> {
|
||||
declinedAction.value = Async.Uninitialized
|
||||
decliningInvite.value?.let {
|
||||
localCoroutineScope.declineInvite(it.roomId, declinedAction)
|
||||
}
|
||||
decliningInvite.value = null
|
||||
}
|
||||
|
||||
is InviteListEvents.CancelDeclineInvite -> {
|
||||
decliningInvite.value = null
|
||||
}
|
||||
|
||||
is InviteListEvents.DismissAcceptError -> {
|
||||
acceptedAction.value = Async.Uninitialized
|
||||
}
|
||||
|
||||
is InviteListEvents.DismissDeclineError -> {
|
||||
declinedAction.value = Async.Uninitialized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return InviteListState(
|
||||
inviteList = invites.mapNotNull(::toInviteSummary).toPersistentList(),
|
||||
declineConfirmationDialog = decliningInvite.value?.let {
|
||||
InviteDeclineConfirmationDialog.Visible(
|
||||
isDirect = it.isDirect,
|
||||
name = it.roomName,
|
||||
)
|
||||
} ?: InviteDeclineConfirmationDialog.Hidden,
|
||||
acceptedAction = acceptedAction.value,
|
||||
declinedAction = declinedAction.value,
|
||||
eventSink = ::handleEvent
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<Async<RoomId>>) = launch {
|
||||
suspend {
|
||||
client.getRoom(roomId)?.acceptInvitation()?.getOrThrow()
|
||||
roomId
|
||||
}.execute(acceptedAction)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<Async<Unit>>) = launch {
|
||||
suspend {
|
||||
client.getRoom(roomId)?.rejectInvitation()?.getOrThrow() ?: Unit
|
||||
}.execute(declinedAction)
|
||||
}
|
||||
|
||||
private fun toInviteSummary(roomSummary: RoomSummary): InviteListInviteSummary? {
|
||||
return when (roomSummary) {
|
||||
is RoomSummary.Filled -> roomSummary.details.run {
|
||||
@@ -71,6 +140,7 @@ class InviteListPresenter @Inject constructor(
|
||||
roomName = name,
|
||||
roomAlias = alias,
|
||||
roomAvatarData = avatarData,
|
||||
isDirect = isDirect,
|
||||
sender = if (isDirect) null else inviter?.let {
|
||||
InviteSender(
|
||||
userId = it.userId,
|
||||
@@ -81,9 +151,10 @@ class InviteListPresenter @Inject constructor(
|
||||
url = it.avatarUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,20 @@ package io.element.android.features.invitelist.impl
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
data class InviteListState(
|
||||
val inviteList: ImmutableList<InviteListInviteSummary>
|
||||
val inviteList: ImmutableList<InviteListInviteSummary>,
|
||||
val declineConfirmationDialog: InviteDeclineConfirmationDialog = InviteDeclineConfirmationDialog.Hidden,
|
||||
val acceptedAction: Async<RoomId> = Async.Uninitialized,
|
||||
val declinedAction: Async<Unit> = Async.Uninitialized,
|
||||
val eventSink: (InviteListEvents) -> Unit = {}
|
||||
)
|
||||
|
||||
sealed interface InviteDeclineConfirmationDialog {
|
||||
object Hidden : InviteDeclineConfirmationDialog
|
||||
data class Visible(val isDirect: Boolean, val name: String) : InviteDeclineConfirmationDialog
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package io.element.android.features.invitelist.impl
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invitelist.impl.model.InviteSender
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -28,7 +29,11 @@ open class InviteListStateProvider : PreviewParameterProvider<InviteListState> {
|
||||
override val values: Sequence<InviteListState>
|
||||
get() = sequenceOf(
|
||||
aInviteListState(),
|
||||
aInviteListState().copy(inviteList = persistentListOf())
|
||||
aInviteListState().copy(inviteList = persistentListOf()),
|
||||
aInviteListState().copy(declineConfirmationDialog = InviteDeclineConfirmationDialog.Visible(true, "Alice")),
|
||||
aInviteListState().copy(declineConfirmationDialog = InviteDeclineConfirmationDialog.Visible(false, "Some Room")),
|
||||
aInviteListState().copy(acceptedAction = Async.Failure(Throwable("Whoops"))),
|
||||
aInviteListState().copy(declinedAction = Async.Failure(Throwable("Whoops"))),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -33,7 +34,10 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.features.invitelist.impl.components.InviteSummaryRow
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
@@ -47,16 +51,56 @@ fun InviteListView(
|
||||
state: InviteListState,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackClicked: () -> Unit = {},
|
||||
onAcceptClicked: (RoomId) -> Unit = {},
|
||||
onDeclineClicked: (RoomId) -> Unit = {},
|
||||
onInviteAccepted: (RoomId) -> Unit = {},
|
||||
) {
|
||||
if (state.acceptedAction is Async.Success) {
|
||||
LaunchedEffect(state.acceptedAction) {
|
||||
onInviteAccepted(state.acceptedAction.state)
|
||||
}
|
||||
}
|
||||
|
||||
InviteListContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onBackClicked = onBackClicked,
|
||||
onAcceptClicked = onAcceptClicked,
|
||||
onDeclineClicked = onDeclineClicked,
|
||||
)
|
||||
|
||||
if (state.declineConfirmationDialog is InviteDeclineConfirmationDialog.Visible) {
|
||||
val contentResource = if (state.declineConfirmationDialog.isDirect)
|
||||
R.string.screen_invites_decline_direct_chat_message
|
||||
else
|
||||
R.string.screen_invites_decline_chat_message
|
||||
|
||||
val titleResource = if (state.declineConfirmationDialog.isDirect)
|
||||
R.string.screen_invites_decline_direct_chat_title
|
||||
else
|
||||
R.string.screen_invites_decline_chat_title
|
||||
|
||||
ConfirmationDialog(
|
||||
content = stringResource(contentResource, state.declineConfirmationDialog.name),
|
||||
title = stringResource(titleResource),
|
||||
onSubmitClicked = { state.eventSink(InviteListEvents.ConfirmDeclineInvite) },
|
||||
onDismiss = { state.eventSink(InviteListEvents.CancelDeclineInvite) }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.acceptedAction is Async.Failure) {
|
||||
ErrorDialog(
|
||||
content = stringResource(StringR.string.error_unknown),
|
||||
title = stringResource(StringR.string.common_error),
|
||||
submitText = stringResource(StringR.string.action_ok),
|
||||
onDismiss = { state.eventSink(InviteListEvents.DismissAcceptError) }
|
||||
)
|
||||
}
|
||||
|
||||
if (state.declinedAction is Async.Failure) {
|
||||
ErrorDialog(
|
||||
content = stringResource(StringR.string.error_unknown),
|
||||
title = stringResource(StringR.string.common_error),
|
||||
submitText = stringResource(StringR.string.action_ok),
|
||||
onDismiss = { state.eventSink(InviteListEvents.DismissDeclineError) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -65,8 +109,6 @@ fun InviteListContent(
|
||||
state: InviteListState,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackClicked: () -> Unit = {},
|
||||
onAcceptClicked: (RoomId) -> Unit = {},
|
||||
onDeclineClicked: (RoomId) -> Unit = {},
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
@@ -102,8 +144,8 @@ fun InviteListContent(
|
||||
) { invite ->
|
||||
InviteSummaryRow(
|
||||
invite = invite,
|
||||
onAcceptClicked = onAcceptClicked,
|
||||
onDeclineClicked = onDeclineClicked,
|
||||
onAcceptClicked = { state.eventSink(InviteListEvents.AcceptInvite(invite)) },
|
||||
onDeclineClicked = { state.eventSink(InviteListEvents.DeclineInvite(invite)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@@ -69,8 +68,8 @@ private val minHeight = 72.dp
|
||||
internal fun InviteSummaryRow(
|
||||
invite: InviteListInviteSummary,
|
||||
modifier: Modifier = Modifier,
|
||||
onAcceptClicked: (RoomId) -> Unit = {},
|
||||
onDeclineClicked: (RoomId) -> Unit = {},
|
||||
onAcceptClicked: () -> Unit = {},
|
||||
onDeclineClicked: () -> Unit = {},
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
@@ -88,8 +87,8 @@ internal fun InviteSummaryRow(
|
||||
@Composable
|
||||
internal fun DefaultInviteSummaryRow(
|
||||
invite: InviteListInviteSummary,
|
||||
onAcceptClicked: (RoomId) -> Unit = {},
|
||||
onDeclineClicked: (RoomId) -> Unit = {},
|
||||
onAcceptClicked: () -> Unit = {},
|
||||
onDeclineClicked: () -> Unit = {},
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -138,7 +137,7 @@ internal fun DefaultInviteSummaryRow(
|
||||
Row(Modifier.padding(top = 12.dp)) {
|
||||
OutlinedButton(
|
||||
content = { Text(stringResource(StringR.string.action_decline), style = ElementTextStyles.Button) },
|
||||
onClick = { onDeclineClicked(invite.roomId) },
|
||||
onClick = onDeclineClicked,
|
||||
modifier = Modifier.weight(1f),
|
||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 7.dp),
|
||||
)
|
||||
@@ -147,7 +146,7 @@ internal fun DefaultInviteSummaryRow(
|
||||
|
||||
Button(
|
||||
content = { Text(stringResource(StringR.string.action_accept), style = ElementTextStyles.Button) },
|
||||
onClick = { onAcceptClicked(invite.roomId) },
|
||||
onClick = onAcceptClicked,
|
||||
modifier = Modifier.weight(1f),
|
||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 7.dp),
|
||||
)
|
||||
|
||||
@@ -27,7 +27,8 @@ data class InviteListInviteSummary(
|
||||
val roomName: String = "",
|
||||
val roomAlias: String? = null,
|
||||
val roomAvatarData: AvatarData = AvatarData(roomId.value, roomName),
|
||||
val sender: InviteSender? = null
|
||||
val sender: InviteSender? = null,
|
||||
val isDirect: Boolean = false
|
||||
)
|
||||
|
||||
data class InviteSender(
|
||||
|
||||
@@ -20,7 +20,9 @@ import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.room.RoomSummary
|
||||
@@ -32,6 +34,7 @@ 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.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -80,32 +83,7 @@ class InviteListPresenterTests {
|
||||
|
||||
@Test
|
||||
fun `present - uses user ID and avatar for direct invites`() = runTest {
|
||||
val invitesDataSource = FakeRoomSummaryDataSource()
|
||||
invitesDataSource.postRoomSummary(
|
||||
listOf(
|
||||
RoomSummary.Filled(
|
||||
RoomSummaryDetails(
|
||||
roomId = A_ROOM_ID,
|
||||
name = A_USER_NAME,
|
||||
avatarURLString = null,
|
||||
isDirect = true,
|
||||
lastMessage = null,
|
||||
lastMessageTimestamp = null,
|
||||
unreadNotificationCount = 0,
|
||||
inviter = RoomMember(
|
||||
userId = A_USER_ID,
|
||||
displayName = A_USER_NAME,
|
||||
avatarUrl = AN_AVATAR_URL,
|
||||
membership = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous = false,
|
||||
powerLevel = 0,
|
||||
normalizedPowerLevel = 0,
|
||||
isIgnored = false,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val invitesDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation()
|
||||
val presenter = InviteListPresenter(
|
||||
FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
@@ -119,7 +97,7 @@ class InviteListPresenterTests {
|
||||
Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
||||
Truth.assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
||||
Truth.assertThat(withInviteState.inviteList[0].roomAlias).isEqualTo(A_USER_ID.value)
|
||||
Truth.assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_USER_NAME)
|
||||
Truth.assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
|
||||
Truth.assertThat(withInviteState.inviteList[0].roomAvatarData).isEqualTo(
|
||||
AvatarData(
|
||||
id = A_USER_ID.value,
|
||||
@@ -133,32 +111,8 @@ class InviteListPresenterTests {
|
||||
|
||||
@Test
|
||||
fun `present - includes sender details for room invites`() = runTest {
|
||||
val invitesDataSource = FakeRoomSummaryDataSource()
|
||||
invitesDataSource.postRoomSummary(
|
||||
listOf(
|
||||
RoomSummary.Filled(
|
||||
RoomSummaryDetails(
|
||||
roomId = A_ROOM_ID,
|
||||
name = A_USER_NAME,
|
||||
avatarURLString = null,
|
||||
isDirect = false,
|
||||
lastMessage = null,
|
||||
lastMessageTimestamp = null,
|
||||
unreadNotificationCount = 0,
|
||||
inviter = RoomMember(
|
||||
userId = A_USER_ID,
|
||||
displayName = A_USER_NAME,
|
||||
avatarUrl = AN_AVATAR_URL,
|
||||
membership = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous = false,
|
||||
powerLevel = 0,
|
||||
normalizedPowerLevel = 0,
|
||||
isIgnored = false,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
|
||||
|
||||
val presenter = InviteListPresenter(
|
||||
FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
@@ -181,4 +135,307 @@ class InviteListPresenterTests {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - shows confirm dialog for declining direct chat invites`() = runTest {
|
||||
val invitesDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation()
|
||||
|
||||
val presenter = InviteListPresenter(
|
||||
FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
invitesDataSource = invitesDataSource,
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val originalState = awaitItem()
|
||||
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
||||
|
||||
val newState = awaitItem()
|
||||
Truth.assertThat(newState.declineConfirmationDialog).isInstanceOf(InviteDeclineConfirmationDialog.Visible::class.java)
|
||||
|
||||
val confirmDialog = newState.declineConfirmationDialog as InviteDeclineConfirmationDialog.Visible
|
||||
Truth.assertThat(confirmDialog.isDirect).isTrue()
|
||||
Truth.assertThat(confirmDialog.name).isEqualTo(A_ROOM_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - shows confirm dialog for declining room invites`() = runTest {
|
||||
val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
|
||||
|
||||
val presenter = InviteListPresenter(
|
||||
FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
invitesDataSource = invitesDataSource,
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val originalState = awaitItem()
|
||||
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
||||
|
||||
val newState = awaitItem()
|
||||
Truth.assertThat(newState.declineConfirmationDialog).isInstanceOf(InviteDeclineConfirmationDialog.Visible::class.java)
|
||||
|
||||
val confirmDialog = newState.declineConfirmationDialog as InviteDeclineConfirmationDialog.Visible
|
||||
Truth.assertThat(confirmDialog.isDirect).isFalse()
|
||||
Truth.assertThat(confirmDialog.name).isEqualTo(A_ROOM_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - hides confirm dialog when cancelling`() = runTest {
|
||||
val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
|
||||
|
||||
val presenter = InviteListPresenter(
|
||||
FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
invitesDataSource = invitesDataSource,
|
||||
)
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val originalState = awaitItem()
|
||||
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
||||
|
||||
skipItems(1)
|
||||
|
||||
originalState.eventSink(InviteListEvents.CancelDeclineInvite)
|
||||
|
||||
val newState = awaitItem()
|
||||
Truth.assertThat(newState.declineConfirmationDialog).isInstanceOf(InviteDeclineConfirmationDialog.Hidden::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - declines invite after confirming`() = runTest {
|
||||
val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
|
||||
val client = FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
invitesDataSource = invitesDataSource,
|
||||
)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = InviteListPresenter(client)
|
||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val originalState = awaitItem()
|
||||
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
||||
|
||||
skipItems(1)
|
||||
|
||||
originalState.eventSink(InviteListEvents.ConfirmDeclineInvite)
|
||||
|
||||
skipItems(2)
|
||||
|
||||
Truth.assertThat(room.isInviteRejected).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - declines invite after confirming and sets state on error`() = runTest {
|
||||
val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
|
||||
val client = FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
invitesDataSource = invitesDataSource,
|
||||
)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = InviteListPresenter(client)
|
||||
val ex = Throwable("Ruh roh!")
|
||||
room.givenRejectInviteResult(Result.failure(ex))
|
||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val originalState = awaitItem()
|
||||
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
||||
|
||||
skipItems(1)
|
||||
|
||||
originalState.eventSink(InviteListEvents.ConfirmDeclineInvite)
|
||||
|
||||
skipItems(1)
|
||||
|
||||
val newState = awaitItem()
|
||||
|
||||
Truth.assertThat(room.isInviteRejected).isTrue()
|
||||
Truth.assertThat(newState.declinedAction).isEqualTo(Async.Failure<Unit>(ex))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - dismisses declining error state`() = runTest {
|
||||
val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
|
||||
val client = FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
invitesDataSource = invitesDataSource,
|
||||
)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = InviteListPresenter(client)
|
||||
val ex = Throwable("Ruh roh!")
|
||||
room.givenRejectInviteResult(Result.failure(ex))
|
||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val originalState = awaitItem()
|
||||
originalState.eventSink(InviteListEvents.DeclineInvite(originalState.inviteList[0]))
|
||||
|
||||
skipItems(1)
|
||||
|
||||
originalState.eventSink(InviteListEvents.ConfirmDeclineInvite)
|
||||
|
||||
skipItems(2)
|
||||
|
||||
originalState.eventSink(InviteListEvents.DismissDeclineError)
|
||||
|
||||
val newState = awaitItem()
|
||||
|
||||
Truth.assertThat(newState.declinedAction).isEqualTo(Async.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - accepts invites and sets state on success`() = runTest {
|
||||
val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
|
||||
val client = FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
invitesDataSource = invitesDataSource,
|
||||
)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = InviteListPresenter(client)
|
||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val originalState = awaitItem()
|
||||
originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0]))
|
||||
|
||||
val newState = awaitItem()
|
||||
|
||||
Truth.assertThat(room.isInviteAccepted).isTrue()
|
||||
Truth.assertThat(newState.acceptedAction).isEqualTo(Async.Success(A_ROOM_ID))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - accepts invites and sets state on error`() = runTest {
|
||||
val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
|
||||
val client = FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
invitesDataSource = invitesDataSource,
|
||||
)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = InviteListPresenter(client)
|
||||
val ex = Throwable("Ruh roh!")
|
||||
room.givenAcceptInviteResult(Result.failure(ex))
|
||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val originalState = awaitItem()
|
||||
originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0]))
|
||||
|
||||
val newState = awaitItem()
|
||||
|
||||
Truth.assertThat(room.isInviteAccepted).isTrue()
|
||||
Truth.assertThat(newState.acceptedAction).isEqualTo(Async.Failure<RoomId>(ex))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - dismisses accepting error state`() = runTest {
|
||||
val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
|
||||
val client = FakeMatrixClient(
|
||||
sessionId = A_SESSION_ID,
|
||||
invitesDataSource = invitesDataSource,
|
||||
)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = InviteListPresenter(client)
|
||||
val ex = Throwable("Ruh roh!")
|
||||
room.givenAcceptInviteResult(Result.failure(ex))
|
||||
client.givenGetRoomResult(A_ROOM_ID, room)
|
||||
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val originalState = awaitItem()
|
||||
originalState.eventSink(InviteListEvents.AcceptInvite(originalState.inviteList[0]))
|
||||
|
||||
skipItems(1)
|
||||
|
||||
originalState.eventSink(InviteListEvents.DismissAcceptError)
|
||||
|
||||
val newState = awaitItem()
|
||||
Truth.assertThat(newState.acceptedAction).isEqualTo(Async.Uninitialized)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun FakeRoomSummaryDataSource.withRoomInvitation(): FakeRoomSummaryDataSource {
|
||||
postRoomSummary(
|
||||
listOf(
|
||||
RoomSummary.Filled(
|
||||
RoomSummaryDetails(
|
||||
roomId = A_ROOM_ID,
|
||||
name = A_ROOM_NAME,
|
||||
avatarURLString = null,
|
||||
isDirect = false,
|
||||
lastMessage = null,
|
||||
lastMessageTimestamp = null,
|
||||
unreadNotificationCount = 0,
|
||||
inviter = RoomMember(
|
||||
userId = A_USER_ID,
|
||||
displayName = A_USER_NAME,
|
||||
avatarUrl = AN_AVATAR_URL,
|
||||
membership = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous = false,
|
||||
powerLevel = 0,
|
||||
normalizedPowerLevel = 0,
|
||||
isIgnored = false,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
private suspend fun FakeRoomSummaryDataSource.withDirectChatInvitation(): FakeRoomSummaryDataSource {
|
||||
postRoomSummary(
|
||||
listOf(
|
||||
RoomSummary.Filled(
|
||||
RoomSummaryDetails(
|
||||
roomId = A_ROOM_ID,
|
||||
name = A_ROOM_NAME,
|
||||
avatarURLString = null,
|
||||
isDirect = true,
|
||||
lastMessage = null,
|
||||
lastMessageTimestamp = null,
|
||||
unreadNotificationCount = 0,
|
||||
inviter = RoomMember(
|
||||
userId = A_USER_ID,
|
||||
displayName = A_USER_NAME,
|
||||
avatarUrl = AN_AVATAR_URL,
|
||||
membership = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous = false,
|
||||
powerLevel = 0,
|
||||
normalizedPowerLevel = 0,
|
||||
isIgnored = false,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.io.Closeable
|
||||
|
||||
interface MatrixRoom: Closeable {
|
||||
interface MatrixRoom : Closeable {
|
||||
val roomId: RoomId
|
||||
val name: String?
|
||||
val bestName: String
|
||||
@@ -36,7 +36,7 @@ interface MatrixRoom: Closeable {
|
||||
val isDirect: Boolean
|
||||
val isPublic: Boolean
|
||||
|
||||
suspend fun members() : List<RoomMember>
|
||||
suspend fun members(): List<RoomMember>
|
||||
|
||||
suspend fun memberCount(): Int
|
||||
|
||||
@@ -63,4 +63,8 @@ interface MatrixRoom: Closeable {
|
||||
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
|
||||
|
||||
suspend fun leave(): Result<Unit>
|
||||
|
||||
suspend fun acceptInvitation(): Result<Unit>
|
||||
|
||||
suspend fun rejectInvitation(): Result<Unit>
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
|
||||
roomId = RoomId(slidingSyncRoom.roomId()),
|
||||
name = slidingSyncRoom.name() ?: slidingSyncRoom.roomId(),
|
||||
canonicalAlias = room?.canonicalAlias(),
|
||||
isDirect = slidingSyncRoom.isDm() ?: false,
|
||||
isDirect = room?.isDirect() ?: false,
|
||||
avatarURLString = room?.avatarUrl(),
|
||||
unreadNotificationCount = slidingSyncRoom.unreadNotifications().use { it.notificationCount().toInt() },
|
||||
lastMessage = latestRoomMessage,
|
||||
|
||||
@@ -206,4 +206,17 @@ class RustMatrixRoom(
|
||||
innerRoom.leave()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun acceptInvitation(): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
kotlin.runCatching {
|
||||
innerRoom.acceptInvitation()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun rejectInvitation(): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
kotlin.runCatching {
|
||||
innerRoom.rejectInvitation()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -52,9 +52,10 @@ class FakeMatrixClient(
|
||||
private var createDmFailure: Throwable? = null
|
||||
private var findDmResult: MatrixRoom? = FakeMatrixRoom()
|
||||
private var logoutFailure: Throwable? = null
|
||||
private val getRoomResults = mutableMapOf<RoomId, MatrixRoom>()
|
||||
|
||||
override fun getRoom(roomId: RoomId): MatrixRoom? {
|
||||
return FakeMatrixRoom(roomId)
|
||||
return getRoomResults[roomId]
|
||||
}
|
||||
|
||||
override fun findDM(userId: UserId): MatrixRoom? {
|
||||
@@ -136,4 +137,8 @@ class FakeMatrixClient(
|
||||
fun givenFindDmResult(result: MatrixRoom?) {
|
||||
findDmResult = result
|
||||
}
|
||||
|
||||
fun givenGetRoomResult(roomId: RoomId, result: MatrixRoom) {
|
||||
getRoomResults[roomId] = result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
@@ -46,12 +46,20 @@ class FakeMatrixRoom(
|
||||
|
||||
private var userDisplayNameResult = Result.success<String?>(null)
|
||||
private var userAvatarUrlResult = Result.success<String?>(null)
|
||||
private var acceptInviteResult = Result.success(Unit)
|
||||
private var rejectInviteResult = Result.success(Unit)
|
||||
private var dmMember: RoomMember? = null
|
||||
private var fetchMemberResult: Result<Unit> = Result.success(Unit)
|
||||
|
||||
var areMembersFetched: Boolean = false
|
||||
private set
|
||||
|
||||
var isInviteAccepted: Boolean = false
|
||||
private set
|
||||
|
||||
var isInviteRejected: Boolean = false
|
||||
private set
|
||||
|
||||
private var leaveRoomError: Throwable? = null
|
||||
|
||||
override fun syncUpdateFlow(): Flow<Long> {
|
||||
@@ -131,6 +139,15 @@ class FakeMatrixRoom(
|
||||
}
|
||||
|
||||
override suspend fun leave(): Result<Unit> = leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit)
|
||||
override suspend fun acceptInvitation(): Result<Unit> {
|
||||
isInviteAccepted = true
|
||||
return acceptInviteResult
|
||||
}
|
||||
|
||||
override suspend fun rejectInvitation(): Result<Unit> {
|
||||
isInviteRejected = true
|
||||
return rejectInviteResult
|
||||
}
|
||||
|
||||
override fun close() = Unit
|
||||
|
||||
@@ -153,4 +170,13 @@ class FakeMatrixRoom(
|
||||
fun givenUserAvatarUrlResult(avatarUrl: Result<String?>) {
|
||||
userAvatarUrlResult = avatarUrl
|
||||
}
|
||||
|
||||
fun givenAcceptInviteResult(result: Result<Unit>) {
|
||||
acceptInviteResult = result
|
||||
}
|
||||
|
||||
fun givenRejectInviteResult(result: Result<Unit>) {
|
||||
rejectInviteResult = result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user