diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index dce2a1071b..b8c2c7742a 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -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) diff --git a/changelog.d/106.feature b/changelog.d/106.feature new file mode 100644 index 0000000000..9507f6ea9c --- /dev/null +++ b/changelog.d/106.feature @@ -0,0 +1 @@ +[Create and join rooms] Accept or decline an invite from invitation list diff --git a/features/invitelist/api/build.gradle.kts b/features/invitelist/api/build.gradle.kts index 65c1ab64f6..6ea2b8a49d 100644 --- a/features/invitelist/api/build.gradle.kts +++ b/features/invitelist/api/build.gradle.kts @@ -24,4 +24,5 @@ android { dependencies { implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) } diff --git a/features/invitelist/api/src/main/kotlin/io/element/android/features/invitelist/api/InviteListEntryPoint.kt b/features/invitelist/api/src/main/kotlin/io/element/android/features/invitelist/api/InviteListEntryPoint.kt index 8ae4df02e7..790aac39be 100644 --- a/features/invitelist/api/src/main/kotlin/io/element/android/features/invitelist/api/InviteListEntryPoint.kt +++ b/features/invitelist/api/src/main/kotlin/io/element/android/features/invitelist/api/InviteListEntryPoint.kt @@ -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) } } diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListEvents.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListEvents.kt new file mode 100644 index 0000000000..0b8f03b45a --- /dev/null +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListEvents.kt @@ -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 + +} diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListNode.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListNode.kt index 4c31334e0b..2f67f83994 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListNode.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListNode.kt @@ -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().forEach { it.onBackClicked() } } + private fun onInviteAccepted(roomId: RoomId) { + plugins().forEach { it.onInviteAccepted(roomId) } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() InviteListView( state = state, onBackClicked = ::onBackClicked, + onInviteAccepted = ::onInviteAccepted, ) } } diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt index 5ed50065a0..c01d85f55f 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListPresenter.kt @@ -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> = remember { mutableStateOf(Async.Uninitialized) } + val declinedAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + val decliningInvite: MutableState = 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>) = launch { + suspend { + client.getRoom(roomId)?.acceptInvitation()?.getOrThrow() + roomId + }.execute(acceptedAction) + } + + private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState>) = 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 } } diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt index a668c86990..5a7761ebc0 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListState.kt @@ -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 + val inviteList: ImmutableList, + val declineConfirmationDialog: InviteDeclineConfirmationDialog = InviteDeclineConfirmationDialog.Hidden, + val acceptedAction: Async = Async.Uninitialized, + val declinedAction: Async = Async.Uninitialized, + val eventSink: (InviteListEvents) -> Unit = {} ) + +sealed interface InviteDeclineConfirmationDialog { + object Hidden : InviteDeclineConfirmationDialog + data class Visible(val isDirect: Boolean, val name: String) : InviteDeclineConfirmationDialog +} diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListStateProvider.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListStateProvider.kt index 43a5ca32ba..d4d1f5c166 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListStateProvider.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListStateProvider.kt @@ -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 { override val values: Sequence 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"))), ) } diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt index 0a28a7838b..78c4c140a1 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListView.kt @@ -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)) }, ) } } diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt index 8bb8ca9207..b5d35ef58a 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/components/InviteSummaryRow.kt @@ -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), ) diff --git a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt index 071cf11bae..f065f28a3a 100644 --- a/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt +++ b/features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/model/InviteListInviteSummary.kt @@ -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( diff --git a/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt b/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt index c4f00437e3..f5f0849991 100644 --- a/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt +++ b/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt @@ -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,13 +111,283 @@ class InviteListPresenterTests { @Test fun `present - includes sender details for room invites`() = runTest { - val invitesDataSource = FakeRoomSummaryDataSource() - invitesDataSource.postRoomSummary( + val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() + + val presenter = InviteListPresenter( + FakeMatrixClient( + sessionId = A_SESSION_ID, + invitesDataSource = invitesDataSource, + ) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val withInviteState = awaitItem() + Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1) + Truth.assertThat(withInviteState.inviteList[0].sender?.displayName).isEqualTo(A_USER_NAME) + Truth.assertThat(withInviteState.inviteList[0].sender?.userId).isEqualTo(A_USER_ID) + Truth.assertThat(withInviteState.inviteList[0].sender?.avatarData).isEqualTo( + AvatarData( + id = A_USER_ID.value, + name = A_USER_NAME, + url = AN_AVATAR_URL, + ) + ) + } + } + + @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(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(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_USER_NAME, + name = A_ROOM_NAME, avatarURLString = null, isDirect = false, lastMessage = null, @@ -159,26 +407,35 @@ class InviteListPresenterTests { ) ) ) - val presenter = InviteListPresenter( - FakeMatrixClient( - sessionId = A_SESSION_ID, - invitesDataSource = invitesDataSource, - ) - ) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val withInviteState = awaitItem() - Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1) - Truth.assertThat(withInviteState.inviteList[0].sender?.displayName).isEqualTo(A_USER_NAME) - Truth.assertThat(withInviteState.inviteList[0].sender?.userId).isEqualTo(A_USER_ID) - Truth.assertThat(withInviteState.inviteList[0].sender?.avatarData).isEqualTo( - AvatarData( - id = A_USER_ID.value, - name = A_USER_NAME, - url = AN_AVATAR_URL, + 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 } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index d4338bb497..70980cc753 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -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 + suspend fun members(): List suspend fun memberCount(): Int @@ -63,4 +63,8 @@ interface MatrixRoom: Closeable { suspend fun redactEvent(eventId: EventId, reason: String? = null): Result suspend fun leave(): Result + + suspend fun acceptInvitation(): Result + + suspend fun rejectInvitation(): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSummaryDetailsFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSummaryDetailsFactory.kt index 93a7d55cd5..1f8be2fa21 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSummaryDetailsFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomSummaryDetailsFactory.kt @@ -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, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index cc0eac2e1f..8afa3cb4ed 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -206,4 +206,17 @@ class RustMatrixRoom( innerRoom.leave() } } + + override suspend fun acceptInvitation(): Result = withContext(coroutineDispatchers.io) { + kotlin.runCatching { + innerRoom.acceptInvitation() + } + } + + override suspend fun rejectInvitation(): Result = withContext(coroutineDispatchers.io) { + kotlin.runCatching { + innerRoom.rejectInvitation() + } + } + } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index c2a234e3de..d33bd5c939 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -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() 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 + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 902f41a80c..05890f9e4a 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -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(null) private var userAvatarUrlResult = Result.success(null) + private var acceptInviteResult = Result.success(Unit) + private var rejectInviteResult = Result.success(Unit) private var dmMember: RoomMember? = null private var fetchMemberResult: Result = 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 { @@ -131,6 +139,15 @@ class FakeMatrixRoom( } override suspend fun leave(): Result = leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit) + override suspend fun acceptInvitation(): Result { + isInviteAccepted = true + return acceptInviteResult + } + + override suspend fun rejectInvitation(): Result { + isInviteRejected = true + return rejectInviteResult + } override fun close() = Unit @@ -153,4 +170,13 @@ class FakeMatrixRoom( fun givenUserAvatarUrlResult(avatarUrl: Result) { userAvatarUrlResult = avatarUrl } + + fun givenAcceptInviteResult(result: Result) { + acceptInviteResult = result + } + + fun givenRejectInviteResult(result: Result) { + rejectInviteResult = result + } + } diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..94c3b51668 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8e44bafcf2fd7119562d6a92a8deff74a7ba470b4a7a87951cf77e19eee2eb0 +size 43576 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3d7a087e66 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b72ad15218e4f17b1c47018925194f3b21c70630bf8744395352707dc257ab70 +size 44025 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7ab4a16b81 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac02a683f4a6297427e6e74bd331c45e9ee7efb044767568bc46566ae46f2274 +size 39618 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7ab4a16b81 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac02a683f4a6297427e6e74bd331c45e9ee7efb044767568bc46566ae46f2274 +size 39618 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..35c6b52fc3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8e00ba1ff01242bf3e1f4858cb66b5ed87681aec76090a545e8d729361e5e37 +size 43034 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fe3e7b9cf6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27b3164386391fc115d3ecdcf202495a3f03509c2c2c5564880ed756d56ed3b5 +size 43463 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..05598c9c54 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64ec3959fddb9dccc271ac0fa28275fa43df893f8f672b99f9f594004cef438e +size 39052 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..05598c9c54 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.invitelist.impl_null_DefaultGroup_RoomListViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64ec3959fddb9dccc271ac0fa28275fa43df893f8f672b99f9f594004cef438e +size 39052