Browse Source
* Add report messages feature * Try to improve how snackbars are delivered --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>feature/julioromano/geocoding_api
Jorge Martin Espinosa
1 year ago
committed by
GitHub
36 changed files with 739 additions and 40 deletions
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
Add option to report inappropriate content |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
/* |
||||
* 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.messages.impl.report |
||||
|
||||
sealed interface ReportMessageEvents { |
||||
data class UpdateReason(val reason: String) : ReportMessageEvents |
||||
object ToggleBlockUser : ReportMessageEvents |
||||
object Report : ReportMessageEvents |
||||
object ClearError : ReportMessageEvents |
||||
} |
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
/* |
||||
* 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.messages.impl.report |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.ui.Modifier |
||||
import com.bumble.appyx.core.modality.BuildContext |
||||
import com.bumble.appyx.core.node.Node |
||||
import com.bumble.appyx.core.plugin.Plugin |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.anvilannotations.ContributesNode |
||||
import io.element.android.libraries.architecture.NodeInputs |
||||
import io.element.android.libraries.architecture.inputs |
||||
import io.element.android.libraries.di.RoomScope |
||||
import io.element.android.libraries.matrix.api.core.EventId |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
|
||||
@ContributesNode(RoomScope::class) |
||||
class ReportMessageNode @AssistedInject constructor( |
||||
@Assisted buildContext: BuildContext, |
||||
@Assisted plugins: List<Plugin>, |
||||
presenterFactory: ReportMessagePresenter.Factory, |
||||
) : Node(buildContext, plugins = plugins) { |
||||
|
||||
data class Inputs( |
||||
val eventId: EventId, |
||||
val senderId: UserId, |
||||
) : NodeInputs |
||||
|
||||
private val inputs = inputs<Inputs>() |
||||
|
||||
private val presenter = presenterFactory.create( |
||||
ReportMessagePresenter.Inputs(inputs.eventId, inputs.senderId) |
||||
) |
||||
|
||||
@Composable |
||||
override fun View(modifier: Modifier) { |
||||
val state = presenter.present() |
||||
ReportMessageView( |
||||
state = state, |
||||
onBackClicked = ::navigateUp, |
||||
modifier = modifier |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,98 @@
@@ -0,0 +1,98 @@
|
||||
/* |
||||
* 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.messages.impl.report |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.MutableState |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.mutableStateOf |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.rememberCoroutineScope |
||||
import androidx.compose.runtime.saveable.rememberSaveable |
||||
import androidx.compose.runtime.setValue |
||||
import dagger.assisted.Assisted |
||||
import dagger.assisted.AssistedFactory |
||||
import dagger.assisted.AssistedInject |
||||
import io.element.android.libraries.architecture.Async |
||||
import io.element.android.libraries.architecture.Presenter |
||||
import io.element.android.libraries.architecture.executeResult |
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers |
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher |
||||
import io.element.android.libraries.designsystem.utils.SnackbarMessage |
||||
import io.element.android.libraries.matrix.api.core.EventId |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom |
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.launch |
||||
import io.element.android.libraries.ui.strings.R as StringR |
||||
|
||||
class ReportMessagePresenter @AssistedInject constructor( |
||||
private val room: MatrixRoom, |
||||
@Assisted private val inputs: Inputs, |
||||
private val snackbarDispatcher: SnackbarDispatcher, |
||||
) : Presenter<ReportMessageState> { |
||||
|
||||
data class Inputs( |
||||
val eventId: EventId, |
||||
val senderId: UserId, |
||||
) |
||||
|
||||
@AssistedFactory |
||||
interface Factory { |
||||
fun create(inputs: Inputs): ReportMessagePresenter |
||||
} |
||||
|
||||
@Composable |
||||
override fun present(): ReportMessageState { |
||||
val coroutineScope = rememberCoroutineScope() |
||||
var reason by rememberSaveable { mutableStateOf("") } |
||||
var blockUser by rememberSaveable { mutableStateOf(false) } |
||||
var result: MutableState<Async<Unit>> = remember { mutableStateOf(Async.Uninitialized) } |
||||
|
||||
fun handleEvents(event: ReportMessageEvents) { |
||||
when (event) { |
||||
is ReportMessageEvents.UpdateReason -> reason = event.reason |
||||
ReportMessageEvents.ToggleBlockUser -> blockUser = !blockUser |
||||
ReportMessageEvents.Report -> coroutineScope.report(inputs.eventId, inputs.senderId, reason, blockUser, result) |
||||
ReportMessageEvents.ClearError -> result.value = Async.Uninitialized |
||||
} |
||||
} |
||||
|
||||
return ReportMessageState( |
||||
reason = reason, |
||||
blockUser = blockUser, |
||||
result = result.value, |
||||
eventSink = ::handleEvents |
||||
) |
||||
} |
||||
|
||||
private fun CoroutineScope.report( |
||||
eventId: EventId, |
||||
userId: UserId, |
||||
reason: String, |
||||
blockUser: Boolean, |
||||
result: MutableState<Async<Unit>>, |
||||
) = launch { |
||||
suspend { |
||||
val userIdToBlock = userId.takeIf { blockUser } |
||||
room.reportContent(eventId, reason, userIdToBlock) |
||||
.onSuccess { |
||||
snackbarDispatcher.post(SnackbarMessage(StringR.string.common_report_submitted)) |
||||
} |
||||
}.executeResult(result) |
||||
} |
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
/* |
||||
* 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.messages.impl.report |
||||
|
||||
import io.element.android.libraries.architecture.Async |
||||
|
||||
data class ReportMessageState( |
||||
val reason: String, |
||||
val blockUser: Boolean, |
||||
val result: Async<Unit>, |
||||
val eventSink: (ReportMessageEvents) -> Unit |
||||
) |
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
/* |
||||
* 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.messages.impl.report |
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider |
||||
import io.element.android.libraries.architecture.Async |
||||
|
||||
open class ReportMessageStateProvider : PreviewParameterProvider<ReportMessageState> { |
||||
override val values: Sequence<ReportMessageState> |
||||
get() = sequenceOf( |
||||
aReportMessageState(), |
||||
aReportMessageState(reason = "This user is making the chat very toxic."), |
||||
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true), |
||||
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = Async.Loading()), |
||||
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = Async.Failure(Throwable())), |
||||
aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = Async.Success(Unit)), |
||||
// Add other states here |
||||
) |
||||
} |
||||
|
||||
fun aReportMessageState( |
||||
reason: String = "", |
||||
blockUser: Boolean = false, |
||||
result: Async<Unit> = Async.Uninitialized, |
||||
) = ReportMessageState( |
||||
reason = reason, |
||||
blockUser = blockUser, |
||||
result = result, |
||||
eventSink = {} |
||||
) |
@ -0,0 +1,186 @@
@@ -0,0 +1,186 @@
|
||||
/* |
||||
* 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.messages.impl.report |
||||
|
||||
import androidx.compose.foundation.layout.Arrangement |
||||
import androidx.compose.foundation.layout.Column |
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi |
||||
import androidx.compose.foundation.layout.Row |
||||
import androidx.compose.foundation.layout.Spacer |
||||
import androidx.compose.foundation.layout.consumeWindowInsets |
||||
import androidx.compose.foundation.layout.fillMaxSize |
||||
import androidx.compose.foundation.layout.fillMaxWidth |
||||
import androidx.compose.foundation.layout.height |
||||
import androidx.compose.foundation.layout.heightIn |
||||
import androidx.compose.foundation.layout.imePadding |
||||
import androidx.compose.foundation.layout.padding |
||||
import androidx.compose.foundation.rememberScrollState |
||||
import androidx.compose.foundation.verticalScroll |
||||
import androidx.compose.material3.ExperimentalMaterial3Api |
||||
import androidx.compose.material3.MaterialTheme |
||||
import androidx.compose.material3.Switch |
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.LaunchedEffect |
||||
import androidx.compose.ui.Modifier |
||||
import androidx.compose.ui.platform.LocalFocusManager |
||||
import androidx.compose.ui.res.stringResource |
||||
import androidx.compose.ui.text.font.FontWeight |
||||
import androidx.compose.ui.text.style.TextAlign |
||||
import androidx.compose.ui.tooling.preview.Preview |
||||
import androidx.compose.ui.tooling.preview.PreviewParameter |
||||
import androidx.compose.ui.unit.dp |
||||
import io.element.android.libraries.architecture.Async |
||||
import io.element.android.libraries.designsystem.ElementTextStyles |
||||
import io.element.android.libraries.designsystem.components.button.BackButton |
||||
import io.element.android.libraries.designsystem.components.button.ButtonWithProgress |
||||
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.CenterAlignedTopAppBar |
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField |
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold |
||||
import io.element.android.libraries.designsystem.theme.components.Text |
||||
import io.element.android.libraries.ui.strings.R as StringR |
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) |
||||
@Composable |
||||
fun ReportMessageView( |
||||
state: ReportMessageState, |
||||
onBackClicked: () -> Unit, |
||||
modifier: Modifier = Modifier, |
||||
) { |
||||
val focusManager = LocalFocusManager.current |
||||
val isSending = state.result is Async.Loading |
||||
when (state.result) { |
||||
is Async.Success -> { |
||||
LaunchedEffect(state.result) { |
||||
onBackClicked() |
||||
} |
||||
return |
||||
} |
||||
is Async.Failure -> { |
||||
ErrorDialog( |
||||
content = stringResource(StringR.string.error_unknown), |
||||
onDismiss = { state.eventSink(ReportMessageEvents.ClearError) } |
||||
) |
||||
} |
||||
else -> Unit |
||||
} |
||||
|
||||
Scaffold( |
||||
topBar = { |
||||
CenterAlignedTopAppBar( |
||||
title = { |
||||
Text( |
||||
stringResource(StringR.string.action_report_content), |
||||
style = ElementTextStyles.Regular.callout, |
||||
fontWeight = FontWeight.Medium, |
||||
) |
||||
}, |
||||
navigationIcon = { |
||||
BackButton(onClick = onBackClicked) |
||||
} |
||||
) |
||||
}, |
||||
modifier = modifier |
||||
) { padding -> |
||||
Column( |
||||
modifier = Modifier |
||||
.padding(padding) |
||||
.consumeWindowInsets(padding) |
||||
.imePadding() |
||||
.fillMaxSize() |
||||
.verticalScroll(rememberScrollState()) |
||||
.padding(16.dp) |
||||
) { |
||||
Spacer(modifier = Modifier.height(20.dp)) |
||||
|
||||
OutlinedTextField( |
||||
value = state.reason, |
||||
onValueChange = { state.eventSink(ReportMessageEvents.UpdateReason(it)) }, |
||||
placeholder = { Text(stringResource(StringR.string.report_content_hint)) }, |
||||
enabled = !isSending, |
||||
modifier = Modifier |
||||
.fillMaxWidth() |
||||
.heightIn(min = 90.dp) |
||||
) |
||||
Text( |
||||
text = stringResource(StringR.string.report_content_explanation), |
||||
style = ElementTextStyles.Regular.caption1, |
||||
color = MaterialTheme.colorScheme.secondary, |
||||
textAlign = TextAlign.Start, |
||||
modifier = Modifier.padding(top = 4.dp, bottom = 24.dp, start = 16.dp, end = 16.dp) |
||||
) |
||||
|
||||
Row( |
||||
modifier = Modifier |
||||
.fillMaxWidth() |
||||
.padding(vertical = 12.dp) |
||||
) { |
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { |
||||
Text( |
||||
text = stringResource(StringR.string.screen_report_content_block_user), |
||||
style = ElementTextStyles.Regular.callout, |
||||
) |
||||
Text( |
||||
text = stringResource(StringR.string.screen_report_content_block_user_hint), |
||||
style = ElementTextStyles.Regular.bodyMD, |
||||
color = MaterialTheme.colorScheme.secondary, |
||||
) |
||||
} |
||||
Switch( |
||||
enabled = !isSending, |
||||
checked = state.blockUser, |
||||
onCheckedChange = { state.eventSink(ReportMessageEvents.ToggleBlockUser) }, |
||||
) |
||||
} |
||||
|
||||
Spacer(modifier = Modifier.height(24.dp)) |
||||
|
||||
ButtonWithProgress( |
||||
text = stringResource(StringR.string.action_send), |
||||
enabled = state.reason.isNotBlank() && !isSending, |
||||
showProgress = isSending, |
||||
onClick = { |
||||
focusManager.clearFocus(force = true) |
||||
state.eventSink(ReportMessageEvents.Report) |
||||
}, |
||||
modifier = Modifier |
||||
.fillMaxWidth() |
||||
.padding(horizontal = 4.dp) |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Preview |
||||
@Composable |
||||
fun ReportMessageViewLightPreview(@PreviewParameter(ReportMessageStateProvider::class) state: ReportMessageState) = |
||||
ElementPreviewLight { ContentToPreview(state) } |
||||
|
||||
@Preview |
||||
@Composable |
||||
fun ReportMessageViewDarkPreview(@PreviewParameter(ReportMessageStateProvider::class) state: ReportMessageState) = |
||||
ElementPreviewDark { ContentToPreview(state) } |
||||
|
||||
@Composable |
||||
private fun ContentToPreview(state: ReportMessageState) { |
||||
ReportMessageView( |
||||
onBackClicked = {}, |
||||
state = state, |
||||
) |
||||
} |
@ -0,0 +1,142 @@
@@ -0,0 +1,142 @@
|
||||
/* |
||||
* 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.messages.report |
||||
|
||||
import app.cash.molecule.RecompositionClock |
||||
import app.cash.molecule.moleculeFlow |
||||
import app.cash.turbine.test |
||||
import com.google.common.truth.Truth.assertThat |
||||
import io.element.android.features.messages.impl.report.ReportMessageEvents |
||||
import io.element.android.features.messages.impl.report.ReportMessagePresenter |
||||
import io.element.android.libraries.architecture.Async |
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher |
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom |
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID |
||||
import io.element.android.libraries.matrix.test.A_USER_ID |
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Test |
||||
|
||||
class ReportMessagePresenterTests { |
||||
|
||||
@Test |
||||
fun `presenter - initial state`() = runTest { |
||||
val presenter = aPresenter() |
||||
moleculeFlow(RecompositionClock.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
assertThat(initialState.reason).isEmpty() |
||||
assertThat(initialState.blockUser).isFalse() |
||||
assertThat(initialState.result).isInstanceOf(Async.Uninitialized::class.java) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `presenter - update reason`() = runTest { |
||||
val presenter = aPresenter() |
||||
moleculeFlow(RecompositionClock.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
val reason = "This user is making the chat very toxic." |
||||
initialState.eventSink(ReportMessageEvents.UpdateReason(reason)) |
||||
|
||||
assertThat(awaitItem().reason).isEqualTo(reason) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `presenter - toggle block user`() = runTest { |
||||
val presenter = aPresenter() |
||||
moleculeFlow(RecompositionClock.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(ReportMessageEvents.ToggleBlockUser) |
||||
|
||||
assertThat(awaitItem().blockUser).isTrue() |
||||
|
||||
initialState.eventSink(ReportMessageEvents.ToggleBlockUser) |
||||
|
||||
assertThat(awaitItem().blockUser).isFalse() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `presenter - handle successful report and block user`() = runTest { |
||||
val room = FakeMatrixRoom() |
||||
val presenter = aPresenter(matrixRoom = room) |
||||
moleculeFlow(RecompositionClock.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(ReportMessageEvents.ToggleBlockUser) |
||||
skipItems(1) |
||||
initialState.eventSink(ReportMessageEvents.Report) |
||||
assertThat(awaitItem().result).isInstanceOf(Async.Loading::class.java) |
||||
assertThat(awaitItem().result).isInstanceOf(Async.Success::class.java) |
||||
assertThat(room.reportedContentCount).isEqualTo(1) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `presenter - handle successful report`() = runTest { |
||||
val room = FakeMatrixRoom() |
||||
val presenter = aPresenter(matrixRoom = room) |
||||
moleculeFlow(RecompositionClock.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(ReportMessageEvents.Report) |
||||
assertThat(awaitItem().result).isInstanceOf(Async.Loading::class.java) |
||||
assertThat(awaitItem().result).isInstanceOf(Async.Success::class.java) |
||||
assertThat(room.reportedContentCount).isEqualTo(1) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `presenter - handle failed report`() = runTest { |
||||
val room = FakeMatrixRoom().apply { |
||||
givenReportContentResult(Result.failure(Exception("Failed to report content"))) |
||||
} |
||||
val presenter = aPresenter(matrixRoom = room) |
||||
moleculeFlow(RecompositionClock.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
val initialState = awaitItem() |
||||
initialState.eventSink(ReportMessageEvents.Report) |
||||
assertThat(awaitItem().result).isInstanceOf(Async.Loading::class.java) |
||||
val resultState = awaitItem() |
||||
assertThat(resultState.result).isInstanceOf(Async.Failure::class.java) |
||||
assertThat(room.reportedContentCount).isEqualTo(1) |
||||
|
||||
resultState.eventSink(ReportMessageEvents.ClearError) |
||||
assertThat(awaitItem().result).isInstanceOf(Async.Uninitialized::class.java) |
||||
} |
||||
} |
||||
|
||||
private fun aPresenter( |
||||
inputs: ReportMessagePresenter.Inputs = ReportMessagePresenter.Inputs(AN_EVENT_ID, A_USER_ID), |
||||
matrixRoom: MatrixRoom = FakeMatrixRoom(), |
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), |
||||
) = ReportMessagePresenter( |
||||
inputs = inputs, |
||||
room = matrixRoom, |
||||
snackbarDispatcher = snackbarDispatcher, |
||||
) |
||||
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue