From 4fd49caca1da65de946a6be2707d7c8ce5a156a4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Apr 2023 05:29:23 +0000 Subject: [PATCH 1/8] Update dependency com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter to v1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7236f1a131..b25ede6676 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -97,7 +97,7 @@ squareup_seismic = "com.squareup:seismic:1.0.3" # network network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:4.10.0" network_retrofit = "com.squareup.retrofit2:retrofit:2.9.0" -network_retrofit_converter_serialization = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" +network_retrofit_converter_serialization = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0" # Test test_core = { module = "androidx.test:core", version.ref = "test_core" } From 041f3d216cd9265f996483b1e8453689e3cc735a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Apr 2023 21:34:16 +0000 Subject: [PATCH 2/8] Update dependency androidx.compose:compose-bom to v2023.04.01 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7236f1a131..7725d59d5c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ activity = "1.7.0" startup = "1.1.1" # Compose -compose_bom = "2023.04.00" +compose_bom = "2023.04.01" composecompiler = "1.4.2" # Coroutines From 9fc46be5be3e2a0fd0b3e3a82eab494cdf2064c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Apr 2023 02:19:56 +0000 Subject: [PATCH 3/8] Update dependency androidx.core:core-splashscreen to v1.0.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7236f1a131..d2c0690388 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ androidx_constraintlayout = { module = "androidx.constraintlayout:constraintlayo androidx_recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } -androidx_splash = "androidx.core:core-splashscreen:1.0.0" +androidx_splash = "androidx.core:core-splashscreen:1.0.1" androidx_security_crypto = "androidx.security:security-crypto:1.0.0" androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" } From e1af1fb9e19e26522d4faf8c32c185533f57bffc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Apr 2023 23:23:19 +0000 Subject: [PATCH 4/8] Update dependency org.matrix.rustcomponents:sdk-android to v0.1.10 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 81ab440d01..ab171103c2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -128,7 +128,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.9" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.10" sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } From ae97a034e5de0935cc3c9c459f1e2213e9d96d85 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 20 Apr 2023 16:13:14 +0100 Subject: [PATCH 5/8] 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 --- .../android/appnav/LoggedInFlowNode.kt | 4 + changelog.d/106.feature | 1 + features/invitelist/api/build.gradle.kts | 1 + .../invitelist/api/InviteListEntryPoint.kt | 3 + .../invitelist/impl/InviteListEvents.kt | 32 ++ .../invitelist/impl/InviteListNode.kt | 6 + .../invitelist/impl/InviteListPresenter.kt | 73 +++- .../invitelist/impl/InviteListState.kt | 13 +- .../impl/InviteListStateProvider.kt | 7 +- .../invitelist/impl/InviteListView.kt | 58 ++- .../impl/components/InviteSummaryRow.kt | 13 +- .../impl/model/InviteListInviteSummary.kt | 3 +- .../impl/InviteListPresenterTests.kt | 363 +++++++++++++++--- .../libraries/matrix/api/room/MatrixRoom.kt | 8 +- .../impl/room/RoomSummaryDetailsFactory.kt | 2 +- .../matrix/impl/room/RustMatrixRoom.kt | 13 + .../libraries/matrix/test/FakeMatrixClient.kt | 7 +- .../matrix/test/room/FakeMatrixRoom.kt | 28 +- ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + 26 files changed, 582 insertions(+), 77 deletions(-) create mode 100644 changelog.d/106.feature create mode 100644 features/invitelist/impl/src/main/kotlin/io/element/android/features/invitelist/impl/InviteListEvents.kt create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 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,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(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_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 + } } 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 From 9f88cb4886111f7eaa235adeb4a7c3b6b6b106c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 08:13:43 +0000 Subject: [PATCH 6/8] Update kotlin --- build.gradle.kts | 2 +- gradle/libs.versions.toml | 6 +++--- libraries/matrix/api/build.gradle.kts | 2 +- libraries/matrix/impl/build.gradle.kts | 2 +- libraries/push/impl/build.gradle.kts | 2 +- libraries/pushproviders/unifiedpush/build.gradle.kts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index eb47692c85..a0648f165a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.cli.common.toBooleanLenient buildscript { dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20") classpath("com.google.gms:google-services:4.3.15") } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1f14815f08..c9a6199811 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,8 +4,8 @@ [versions] # Project android_gradle_plugin = "8.0.0" -kotlin = "1.8.10" -ksp = "1.8.10-1.0.9" +kotlin = "1.8.20" +ksp = "1.8.20-1.0.11" molecule = "0.9.0" # AndroidX @@ -20,7 +20,7 @@ startup = "1.1.1" # Compose compose_bom = "2023.04.01" -composecompiler = "1.4.2" +composecompiler = "1.4.6" # Coroutines coroutines = "1.6.4" diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index fab586d108..54d1406389 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -18,7 +18,7 @@ plugins { id("io.element.android-library") id("kotlin-parcelize") alias(libs.plugins.anvil) - kotlin("plugin.serialization") version "1.8.10" + kotlin("plugin.serialization") version "1.8.20" } android { diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 1fb94425f3..4c11207378 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -17,7 +17,7 @@ plugins { id("io.element.android-library") alias(libs.plugins.anvil) - kotlin("plugin.serialization") version "1.8.10" + kotlin("plugin.serialization") version "1.8.20" } android { diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 242ee04089..2a404d42a8 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -16,7 +16,7 @@ plugins { id("io.element.android-library") alias(libs.plugins.anvil) - kotlin("plugin.serialization") version "1.8.10" + kotlin("plugin.serialization") version "1.8.20" } android { diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts index 8eb6bed0e4..c9711c6ba6 100644 --- a/libraries/pushproviders/unifiedpush/build.gradle.kts +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -16,7 +16,7 @@ plugins { id("io.element.android-library") alias(libs.plugins.anvil) - kotlin("plugin.serialization") version "1.8.10" + kotlin("plugin.serialization") version "1.8.20" } android { From b7757af5d7a86270088c2c5b5ee75fb89750dca1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Apr 2023 08:45:30 +0000 Subject: [PATCH 7/8] Update anvil to v2.4.5 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c9a6199811..0b702caea0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,7 +44,7 @@ sqldelight = "1.5.5" # DI dagger = "2.45" -anvil = "2.4.4" +anvil = "2.4.5" # quality detekt = "1.22.0" From fbc9bcdffc415a7b52da7dea1049abd7b54f583f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 23 Apr 2023 06:32:17 +0000 Subject: [PATCH 8/8] Update dependency com.squareup.okhttp3:okhttp-bom to v4.11.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 75ee820465..275145a865 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -95,7 +95,7 @@ accompanist_flowlayout = { module = "com.google.accompanist:accompanist-flowlayo squareup_seismic = "com.squareup:seismic:1.0.3" # network -network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:4.10.0" +network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:4.11.0" network_retrofit = "com.squareup.retrofit2:retrofit:2.9.0" network_retrofit_converter_serialization = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"