First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
import extension.setupDependencyInjection
import extension.testCommonDependencies
plugins {
id("io.element.android-compose-library")
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.reportroom.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
setupDependencyInjection()
dependencies {
api(projects.features.reportroom.api)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
testCommonDependencies(libs, true)
testImplementation(projects.libraries.matrix.test)
}
@@ -0,0 +1,28 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.reportroom.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.reportroom.api.ReportRoomEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.matrix.api.core.RoomId
@ContributesBinding(AppScope::class)
class DefaultReportRoomEntryPoint : ReportRoomEntryPoint {
override fun createNode(
parentNode: Node,
buildContext: BuildContext,
roomId: RoomId,
): Node {
return parentNode.createNode<ReportRoomNode>(buildContext, plugins = listOf(ReportRoomNode.Inputs(roomId)))
}
}
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.reportroom.impl
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
interface ReportRoom {
suspend operator fun invoke(
roomId: RoomId,
shouldReport: Boolean,
reason: String,
shouldLeave: Boolean,
): Result<Unit>
sealed class Exception : kotlin.Exception() {
data object RoomNotFound : Exception()
data object LeftRoomFailed : Exception()
data object ReportRoomFailed : Exception()
}
}
@ContributesBinding(SessionScope::class)
class DefaultReportRoom(
private val client: MatrixClient,
) : ReportRoom {
override suspend operator fun invoke(
roomId: RoomId,
shouldReport: Boolean,
reason: String,
shouldLeave: Boolean
): Result<Unit> {
val room = client.getRoom(roomId)
?: return Result.failure(ReportRoom.Exception.RoomNotFound)
if (shouldReport) {
room
.reportRoom(reason.takeIf { it.isNotBlank() })
.onFailure {
return Result.failure(ReportRoom.Exception.ReportRoomFailed)
}
}
if (shouldLeave) {
room
.leave()
.onFailure {
return Result.failure(ReportRoom.Exception.LeftRoomFailed)
}
}
return Result.success(Unit)
}
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.reportroom.impl
sealed interface ReportRoomEvents {
data class UpdateReason(val reason: String) : ReportRoomEvents
data object ToggleLeaveRoom : ReportRoomEvents
data object Report : ReportRoomEvents
data object ClearReportAction : ReportRoomEvents
}
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.reportroom.impl
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 dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.core.RoomId
@ContributesNode(SessionScope::class)
@AssistedInject
class ReportRoomNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: ReportRoomPresenter.Factory,
) : Node(buildContext, plugins = plugins) {
data class Inputs(val roomId: RoomId) : NodeInputs
private val roomId = inputs<Inputs>().roomId
private val presenter: ReportRoomPresenter = presenterFactory.create(roomId = roomId)
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
ReportRoomView(
state = state,
modifier = modifier,
onBackClick = ::navigateUp,
)
}
}
@@ -0,0 +1,85 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.reportroom.impl
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 dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingState
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@AssistedInject
class ReportRoomPresenter(
@Assisted private val roomId: RoomId,
private val reportRoom: ReportRoom,
) : Presenter<ReportRoomState> {
@AssistedFactory
fun interface Factory {
fun create(roomId: RoomId): ReportRoomPresenter
}
@Composable
override fun present(): ReportRoomState {
var reason by rememberSaveable { mutableStateOf("") }
var leaveRoom by rememberSaveable { mutableStateOf(false) }
var reportAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
val coroutineScope = rememberCoroutineScope()
fun handleEvent(event: ReportRoomEvents) {
when (event) {
ReportRoomEvents.Report -> coroutineScope.reportRoom(reason, leaveRoom, reportAction)
ReportRoomEvents.ToggleLeaveRoom -> {
leaveRoom = !leaveRoom
}
is ReportRoomEvents.UpdateReason -> {
reason = event.reason
}
ReportRoomEvents.ClearReportAction -> {
reportAction.value = AsyncAction.Uninitialized
}
}
}
return ReportRoomState(
reason = reason,
leaveRoom = leaveRoom,
reportAction = reportAction.value,
eventSink = ::handleEvent,
)
}
private fun CoroutineScope.reportRoom(
reason: String,
shouldLeave: Boolean,
action: MutableState<AsyncAction<Unit>>
) = launch {
val previousFailure = action.value as? AsyncAction.Failure
val shouldReport = previousFailure?.error !is ReportRoom.Exception.LeftRoomFailed
runUpdatingState(action) {
reportRoom(
roomId = roomId,
shouldReport = shouldReport,
reason = reason,
shouldLeave = shouldLeave
)
}
}
}
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.reportroom.impl
import io.element.android.libraries.architecture.AsyncAction
data class ReportRoomState(
val reason: String,
val leaveRoom: Boolean,
val reportAction: AsyncAction<Unit>,
val eventSink: (ReportRoomEvents) -> Unit
) {
val canReport: Boolean = reason.isNotBlank()
}
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.reportroom.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncAction
open class ReportRoomStateProvider : PreviewParameterProvider<ReportRoomState> {
companion object {
private const val A_REPORT_ROOM_REASON = "Inappropriate content"
}
override val values: Sequence<ReportRoomState>
get() = sequenceOf(
aReportRoomState(),
aReportRoomState(reason = A_REPORT_ROOM_REASON),
aReportRoomState(leaveRoom = true),
aReportRoomState(reason = A_REPORT_ROOM_REASON, reportAction = AsyncAction.Loading),
aReportRoomState(reason = A_REPORT_ROOM_REASON, reportAction = AsyncAction.Failure(Exception("Failed to report"))),
)
}
fun aReportRoomState(
reason: String = "",
leaveRoom: Boolean = false,
reportAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (ReportRoomEvents) -> Unit = {}
) = ReportRoomState(
reason = reason,
leaveRoom = leaveRoom,
reportAction = reportAction,
eventSink = eventSink,
)
@@ -0,0 +1,148 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.reportroom.impl
import androidx.compose.foundation.layout.Column
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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.designsystem.components.async.AsyncActionView
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReportRoomView(
state: ReportRoomState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current
val isReporting = state.reportAction is AsyncAction.Loading
AsyncActionView(
async = state.reportAction,
onSuccess = { onBackClick() },
errorTitle = { failure ->
when (failure) {
is ReportRoom.Exception.LeftRoomFailed -> stringResource(R.string.screen_report_room_leave_failed_alert_title)
else -> stringResource(CommonStrings.dialog_title_error)
}
},
errorMessage = { failure ->
when (failure) {
is ReportRoom.Exception.LeftRoomFailed -> stringResource(R.string.screen_report_room_leave_failed_alert_message)
else -> stringResource(CommonStrings.error_unknown)
}
},
onRetry = {
state.eventSink(ReportRoomEvents.Report)
},
onErrorDismiss = { state.eventSink(ReportRoomEvents.ClearReportAction) }
)
Scaffold(
topBar = {
TopAppBar(
titleStr = stringResource(R.string.screen_report_room_title),
navigationIcon = {
BackButton(onClick = onBackClick)
}
)
},
modifier = modifier
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
.imePadding()
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp)
) {
TextField(
value = state.reason,
onValueChange = { state.eventSink(ReportRoomEvents.UpdateReason(it)) },
placeholder = stringResource(R.string.screen_report_room_reason_placeholder),
minLines = 3,
enabled = !isReporting,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.heightIn(min = 90.dp),
supportingText = stringResource(R.string.screen_report_room_reason_footer),
)
Spacer(modifier = Modifier.height(24.dp))
ListItem(
modifier = Modifier.padding(end = 8.dp),
headlineContent = {
Text(text = stringResource(CommonStrings.action_leave_room))
},
onClick = {
state.eventSink(ReportRoomEvents.ToggleLeaveRoom)
},
trailingContent = ListItemContent.Switch(checked = state.leaveRoom)
)
Spacer(modifier = Modifier.height(24.dp))
Button(
text = stringResource(CommonStrings.action_report),
enabled = state.canReport && !isReporting,
destructive = true,
showProgress = isReporting,
onClick = {
focusManager.clearFocus(force = true)
state.eventSink(ReportRoomEvents.Report)
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
}
}
}
@PreviewsDayNight
@Composable
internal fun ReportRoomViewPreview(
@PreviewParameter(ReportRoomStateProvider::class) state: ReportRoomState
) = ElementPreview {
ReportRoomView(
state = state,
onBackClick = {},
)
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_title">"Докладване на стаята"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Vaše hlášení bylo úspěšně odesláno, ale při pokusu o opuštění místnosti jsme narazili na problém. Zkuste to prosím znovu."</string>
<string name="screen_report_room_leave_failed_alert_title">"Nelze opustit místnost"</string>
<string name="screen_report_room_reason_footer">"Nahlaste tuto místnost svému administrátorovi. Pokud jsou zprávy zašifrované, váš administrátor je nebude moci číst."</string>
<string name="screen_report_room_reason_placeholder">"Popište důvod…"</string>
<string name="screen_report_room_title">"Nahlásit místnost"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Cyflwynwyd eich adroddiad yn llwyddiannus, ond cododd problem wrth geisio gadael yr ystafell. Ceisiwch eto."</string>
<string name="screen_report_room_leave_failed_alert_title">"Methu Gadael yr Ystafell"</string>
<string name="screen_report_room_reason_footer">"Adroddwch yr ystafell hon i\'ch gweinyddwr. Os yw\'r negeseuon wedi\'u hamgryptio, fydd eich gweinyddwr ddim yn gallu eu darllen."</string>
<string name="screen_report_room_reason_placeholder">"Disgrifiwch y rheswm…"</string>
<string name="screen_report_room_title">"Adrodd ar ystafell"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Din anmeldelse blev indsendt med succes, men vi stødte på et problem, da vi forsøgte at forlade rummet. Prøv venligst igen."</string>
<string name="screen_report_room_leave_failed_alert_title">"Ude af stand til at forlade rummet"</string>
<string name="screen_report_room_reason_footer">"Anmeld dette rum til din administrator. Hvis meddelelserne er krypteret, kan din administrator ikke læse dem."</string>
<string name="screen_report_room_reason_placeholder">"Beskriv årsagen til anmeldelsen…"</string>
<string name="screen_report_room_title">"Anmeld rummet"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Deine Meldung wurde erfolgreich übermittelt. Beim Versuch, den Chat zu verlassen, ist allerdings ein Problem aufgetreten. Bitte versuche es erneut."</string>
<string name="screen_report_room_leave_failed_alert_title">"Der Chat kann nicht verlassen werden"</string>
<string name="screen_report_room_reason_footer">"Melde diesen Chat deinem Administrator. Wenn die Nachrichten verschlüsselt sind, kann dein Administrator sie nicht lesen."</string>
<string name="screen_report_room_reason_placeholder">"Beschreibe den Grund für die Meldung…"</string>
<string name="screen_report_room_title">"Chat melden"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Η αναφορά σας υποβλήθηκε με επιτυχία, αλλά αντιμετωπίσαμε ένα πρόβλημα κατά την προσπάθεια εξόδου από την αίθουσα. Παρακαλώ προσπαθήστε ξανά."</string>
<string name="screen_report_room_leave_failed_alert_title">"Δεν είναι δυνατή η έξοδος από την αίθουσα"</string>
<string name="screen_report_room_reason_footer">"Αναφέρετε αυτήν την αίθουσα στον διαχειριστή σας. Εάν τα μηνύματα είναι κρυπτογραφημένα, ο διαχειριστής σας δεν θα μπορεί να τα διαβάσει."</string>
<string name="screen_report_room_reason_placeholder">"Περιγράψτε τον λόγο αναφοράς…"</string>
<string name="screen_report_room_title">"Αναφορά αίθουσας"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Tu denuncia se ha enviado correctamente, pero hemos encontrado un problema al intentar salir de la sala. Inténtalo de nuevo."</string>
<string name="screen_report_room_leave_failed_alert_title">"No se pudo salir de la sala"</string>
<string name="screen_report_room_reason_footer">"Denuncia esta sala a tu administrador. Si los mensajes están cifrados, tu administrador no podrá leerlos."</string>
<string name="screen_report_room_reason_placeholder">"Describe el motivo de la denuncia…"</string>
<string name="screen_report_room_title">"Denunciar sala"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Jututoast haldajale teatamine õnnestus, kuid jututost lahkumisel tekkis viga. Palun proovi uuesti lahkuda."</string>
<string name="screen_report_room_leave_failed_alert_title">"Pole võimalik lahkuda jututoast"</string>
<string name="screen_report_room_reason_footer">"Teata sellest jututoast süsteemi haldajale. Kui sõnumid on krüptitud, ei saa haldaja neid lugeda."</string>
<string name="screen_report_room_reason_placeholder">"Kirjelda põhjust…"</string>
<string name="screen_report_room_title">"Teata jututoast"</string>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_title">"Salatu gela"</string>
</resources>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_title">"ناتوان از ترک اتاق"</string>
<string name="screen_report_room_reason_placeholder">"شرح دلیل گزارش…"</string>
<string name="screen_report_room_title">"گزارش اتاق"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Ilmoituksesi lähetettiin onnistuneesti, mutta kohtasimme ongelman yrittäessämme poistua huoneesta. Yritä uudelleen."</string>
<string name="screen_report_room_leave_failed_alert_title">"Huoneesta poistuminen epäonnistui"</string>
<string name="screen_report_room_reason_footer">"Ilmoita tästä huoneesta palvelimesi ylläpitäjälle. Jos viestit on salattu, ylläpitäjäsi ei voi lukea niitä."</string>
<string name="screen_report_room_reason_placeholder">"Kuvaile syytä…"</string>
<string name="screen_report_room_title">"Ilmoita huoneesta"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Votre rapport a été envoyé avec succès, mais nous avons rencontré un problème en essayant de quitter le salon. Veuillez réessayer."</string>
<string name="screen_report_room_leave_failed_alert_title">"Impossible de quitter le salon"</string>
<string name="screen_report_room_reason_footer">"Signaler ce salon à votre admin. Si les messages sont chiffrés, votre admin ne pourra pas les lire."</string>
<string name="screen_report_room_reason_placeholder">"Décrivez la raison du signalement…"</string>
<string name="screen_report_room_title">"Signaler le salon"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"A jelentése sikeresen el lett küldve, de hibát találtunk a szoba elhagyása során. Próbálja újra."</string>
<string name="screen_report_room_leave_failed_alert_title">"Nem tudja elhagyni a szobát"</string>
<string name="screen_report_room_reason_footer">"A szoba jelentése az adminisztrátoroknak. Ha az üzenetek titkosítva vannak, akkor az adminisztrátor nem fogja tudni elolvasni őket."</string>
<string name="screen_report_room_reason_placeholder">"Írja le az okot…"</string>
<string name="screen_report_room_title">"Szoba jelentése"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Laporan Anda berhasil dikirimkan, tetapi kami mengalami masalah saat mencoba meninggalkan ruangan. Silakan coba lagi."</string>
<string name="screen_report_room_leave_failed_alert_title">"Tidak Dapat Meninggalkan Ruangan"</string>
<string name="screen_report_room_reason_footer">"Laporkan ruangan ini ke admin Anda. Jika pesan dienkripsi, admin Anda tidak akan dapat membacanya."</string>
<string name="screen_report_room_reason_placeholder">"Jelaskan alasan untuk melaporkan…"</string>
<string name="screen_report_room_title">"Laporkan ruangan"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"La tua segnalazione è stata inviata con successo, ma abbiamo riscontrato un problema durante il tentativo di lasciare la stanza. Per favore riprova."</string>
<string name="screen_report_room_leave_failed_alert_title">"Impossibile lasciare la stanza"</string>
<string name="screen_report_room_reason_footer">"Segnala questa stanza al tuo amministratore. Se i messaggi sono cifrati, l\'amministratore non sarà in grado di leggerli."</string>
<string name="screen_report_room_reason_placeholder">"Descrivi il motivo della segnalazione…"</string>
<string name="screen_report_room_title">"Segnala stanza"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"신고가 성공적으로 제출되었지만, 방을 나가려고 하는 중에 문제가 발생했습니다. 다시 시도해 주세요."</string>
<string name="screen_report_room_leave_failed_alert_title">"방을 나갈 수 없습니다"</string>
<string name="screen_report_room_reason_footer">"이 방을 관리자에게 신고하세요. 메시지가 암호화되어 있는 경우, 관리자는 메시지를 읽을 수 없습니다."</string>
<string name="screen_report_room_reason_placeholder">"신고 사유를 설명하세요…"</string>
<string name="screen_report_room_title">"방 신고"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Rapporten din ble sendt inn, men vi oppdaget et problem da vi prøvde å forlate rommet. Prøv igjen."</string>
<string name="screen_report_room_leave_failed_alert_title">"Kan ikke forlate rommet"</string>
<string name="screen_report_room_reason_footer">"Rapporter dette rommet til administratoren din. Hvis meldingene er kryptert, vil administratoren ikke kunne lese dem."</string>
<string name="screen_report_room_reason_placeholder">"Beskriv årsaken…"</string>
<string name="screen_report_room_title">"Rapporter rommet"</string>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_title">"Kamer melden"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Twoje zgłoszenie zostało wysłane pomyślnie, ale napotkaliśmy problem podczas opuszczania pokoju. Spróbuj ponownie."</string>
<string name="screen_report_room_leave_failed_alert_title">"Nie można wyjść z pokoju"</string>
<string name="screen_report_room_reason_footer">"Zgłoś ten pokój swojemu administratorowi. Jeśli wiadomości są zaszyfrowane, administrator nie będzie mógł ich odczytać."</string>
<string name="screen_report_room_reason_placeholder">"Opisz powód…"</string>
<string name="screen_report_room_title">"Zgłoś pokój"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Sua denúncia foi enviada com sucesso, mas encontramos um problema ao tentar sair da sala. Tente novamente."</string>
<string name="screen_report_room_leave_failed_alert_title">"Não foi possível sair da sala"</string>
<string name="screen_report_room_reason_footer">"Denuncie esta sala ao seu administrador. Se as mensagens estiverem criptografadas, seu administrador não poderá lê-las."</string>
<string name="screen_report_room_reason_placeholder">"Descreva o motivo para denunciar…"</string>
<string name="screen_report_room_title">"Denunciar sala"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"O teu relatório foi submetido com sucesso, mas houve um problema ao tentar sair da sala. Por favor, tenta novamente."</string>
<string name="screen_report_room_leave_failed_alert_title">"Não foi possível sair da sala"</string>
<string name="screen_report_room_reason_footer">"Denuncia esta sala aos administradores. Se as mensagens estiverem cifradas, os administradores não as poderão ler."</string>
<string name="screen_report_room_reason_placeholder">"Descreve a razão para denunciar…"</string>
<string name="screen_report_room_title">"Denunciar sala"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Raportul dumneavoastră a fost trimis cu succes, dar am întâmpinat o problemă în timp ce încercam să părăsim camera. Vă rugăm să încercați din nou."</string>
<string name="screen_report_room_leave_failed_alert_title">"Nu s-a putut părăsi camera"</string>
<string name="screen_report_room_reason_footer">"Raportați această cameră administratorului. Dacă mesaje sunt criptate, administratorul nu le va putea citi."</string>
<string name="screen_report_room_reason_placeholder">"Descrieți motivul raportării…"</string>
<string name="screen_report_room_title">"Raportați camera"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Ваш отчет был успешно отправлен, но мы столкнулись с проблемой при попытке покинуть комнату. Пожалуйста, попробуйте еще раз."</string>
<string name="screen_report_room_leave_failed_alert_title">"Невозможно покинуть комнату"</string>
<string name="screen_report_room_reason_footer">"Сообщите об этой комнате своему администратору. Если сообщения зашифрованы, ваш администратор не сможет их прочитать."</string>
<string name="screen_report_room_reason_placeholder">"Опишите причину жалобы…"</string>
<string name="screen_report_room_title">"Комната отчетов"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Vaša správa bola úspešne odoslaná, ale pri pokuse o opustenie miestnosti sme narazili na problém. Skúste to prosím znova."</string>
<string name="screen_report_room_leave_failed_alert_title">"Nie je možné opustiť miestnosť"</string>
<string name="screen_report_room_reason_footer">"Nahláste túto miestnosť svojmu správcovi. Ak sú správy zašifrované, váš správca ich nebude môcť prečítať."</string>
<string name="screen_report_room_reason_placeholder">"Popíšte dôvod…"</string>
<string name="screen_report_room_title">"Nahlásiť miestnosť"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Din anmälan skickades in framgångsrikt, men vi stötte på ett problem när vi försökte lämna rummet. Vänligen försök igen."</string>
<string name="screen_report_room_leave_failed_alert_title">"Kunde inte lämna rummet"</string>
<string name="screen_report_room_reason_footer">"Anmäl det här rummet till din administratör. Om meddelandena är krypterade kommer din administratör inte att kunna läsa dem."</string>
<string name="screen_report_room_reason_placeholder">"Beskriv anledningen …"</string>
<string name="screen_report_room_title">"Anmäl rum"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Ваша скарга надіслана, але ми зіткнулися з проблемою під час спроби вийти з кімнати. Повторіть спробу."</string>
<string name="screen_report_room_leave_failed_alert_title">"Не вдалося вийти з кімнати"</string>
<string name="screen_report_room_reason_footer">"Поскаржтеся на цю кімнату своєму адміністратору. Якщо повідомлення зашифровані, ваш адміністратор не зможе їх прочитати."</string>
<string name="screen_report_room_reason_placeholder">"Опишіть причину…"</string>
<string name="screen_report_room_title">"Поскаржитися на кімнату"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Hisobotingiz muvaffaqiyatli yuborildi, ammo xonadan chiqishda muammo yuzaga keldi. Iltimos, qaytadan urinib koring."</string>
<string name="screen_report_room_leave_failed_alert_title">"Xonani tark etish imkonsiz"</string>
<string name="screen_report_room_reason_footer">"Bu xona haqida administratoringizga xabar bering. Agar xabarlar shifrlangan bolsa, administratoringiz ularni oqiy olmaydi."</string>
<string name="screen_report_room_reason_placeholder">"Xabar berish sababini tushuntiring…"</string>
<string name="screen_report_room_title">"Xona ustidan shikoyat qilish"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"您的回報已成功遞交,但我們嘗試離開聊天室時遇到了問題。請再試一次。"</string>
<string name="screen_report_room_leave_failed_alert_title">"無法離開聊天室"</string>
<string name="screen_report_room_reason_footer">"將此聊天室回報給您的管理員。若訊息已加密,您的管理員將無法讀取它們。"</string>
<string name="screen_report_room_reason_placeholder">"說明回報的原因……"</string>
<string name="screen_report_room_title">"回報聊天室"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"您的报告已成功提交,但在尝试离开房间时遇到了问题。请重试。"</string>
<string name="screen_report_room_leave_failed_alert_title">"无法离开房间"</string>
<string name="screen_report_room_reason_footer">"向管理员举报此房间。如果信息已加密,管理员将无法读取。"</string>
<string name="screen_report_room_reason_placeholder">"描述举报的原因…"</string>
<string name="screen_report_room_title">"举报房间"</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_report_room_leave_failed_alert_message">"Your report was submitted successfully, but we encountered an issue while trying to leave the room. Please try again."</string>
<string name="screen_report_room_leave_failed_alert_title">"Unable to Leave Room"</string>
<string name="screen_report_room_reason_footer">"Report this room to your admin. If the messages are encrypted, your admin will not be able to read them."</string>
<string name="screen_report_room_reason_placeholder">"Describe the reason to report…"</string>
<string name="screen_report_room_title">"Report room"</string>
</resources>
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.reportroom.impl
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.tests.testutils.node.TestParentNode
import org.junit.Rule
import org.junit.Test
class DefaultReportRoomEntryPointTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun `test node builder`() {
val entryPoint = DefaultReportRoomEntryPoint()
val parentNode = TestParentNode.create { buildContext, plugins ->
ReportRoomNode(
buildContext = buildContext,
plugins = plugins,
presenterFactory = { roomId ->
assertThat(roomId).isEqualTo(A_ROOM_ID)
createReportRoomPresenter()
}
)
}
val result = entryPoint.createNode(
parentNode = parentNode,
buildContext = BuildContext.root(null),
roomId = A_ROOM_ID,
)
assertThat(result).isInstanceOf(ReportRoomNode::class.java)
assertThat(result.plugins).contains(ReportRoomNode.Inputs(A_ROOM_ID))
}
}
@@ -0,0 +1,151 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.reportroom.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultReportRoomTest {
private val roomId = A_ROOM_ID
private val successLeaveRoomLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
private val successReportRoomLambda =
lambdaRecorder<String?, Result<Unit>> { _ -> Result.success(Unit) }
private val failureLeaveRoomLambda =
lambdaRecorder<Result<Unit>> { Result.failure(Exception("Leave room error")) }
private val failureReportRoomLambda =
lambdaRecorder<String?, Result<Unit>> { _ -> Result.failure(Exception("Report room error")) }
@Test
fun `report room, leave=false, report=false, nothing is called`() = runTest {
val room = FakeBaseRoom(
roomId = roomId,
leaveRoomLambda = successLeaveRoomLambda,
reportRoomResult = successReportRoomLambda
)
val client = FakeMatrixClient().apply {
givenGetRoomResult(roomId, room)
}
val reportRoom = DefaultReportRoom(client = client)
val result = reportRoom(roomId, shouldReport = false, reason = "", shouldLeave = false)
assertThat(result.isSuccess).isTrue()
assert(successLeaveRoomLambda).isNeverCalled()
assert(successReportRoomLambda).isNeverCalled()
}
@Test
fun `report room, leave=false, report=true, report room success`() = runTest {
val room = FakeBaseRoom(
roomId = roomId,
leaveRoomLambda = successLeaveRoomLambda,
reportRoomResult = successReportRoomLambda
)
val client = FakeMatrixClient().apply {
givenGetRoomResult(roomId, room)
}
val reportRoom = DefaultReportRoom(client = client)
val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = false)
assertThat(result.isSuccess).isTrue()
assert(successLeaveRoomLambda).isNeverCalled()
assert(successReportRoomLambda)
.isCalledOnce()
.with(value("Spam"))
}
@Test
fun `report room, leave=true, report=false, leave room success`() = runTest {
val room = FakeBaseRoom(
roomId = roomId,
leaveRoomLambda = successLeaveRoomLambda,
reportRoomResult = successReportRoomLambda
)
val client = FakeMatrixClient().apply {
givenGetRoomResult(roomId, room)
}
val reportRoom = DefaultReportRoom(client = client)
val result = reportRoom(roomId, shouldReport = false, reason = "", shouldLeave = true)
assertThat(result.isSuccess).isTrue()
assert(successLeaveRoomLambda).isCalledOnce()
assert(successReportRoomLambda).isNeverCalled()
}
@Test
fun `report room, leave=true, report=true, leave room success`() = runTest {
val room = FakeBaseRoom(
roomId = roomId,
leaveRoomLambda = successLeaveRoomLambda,
reportRoomResult = successReportRoomLambda
)
val client = FakeMatrixClient().apply {
givenGetRoomResult(roomId, room)
}
val reportRoom = DefaultReportRoom(client = client)
val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = true)
assertThat(result.isSuccess).isTrue()
assert(successLeaveRoomLambda).isCalledOnce()
assert(successReportRoomLambda)
.isCalledOnce()
.with(value("Spam"))
}
@Test
fun `report room, leave=true, report=true, leave room failed`() = runTest {
val room = FakeBaseRoom(
roomId = roomId,
leaveRoomLambda = failureLeaveRoomLambda,
reportRoomResult = successReportRoomLambda
)
val client = FakeMatrixClient().apply {
givenGetRoomResult(roomId, room)
}
val reportRoom = DefaultReportRoom(client = client)
val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = true)
assertThat(result.isFailure).isTrue()
assertThat(result.exceptionOrNull()).isEqualTo(ReportRoom.Exception.LeftRoomFailed)
assert(failureLeaveRoomLambda).isCalledOnce()
assert(successReportRoomLambda).isCalledOnce()
}
@Test
fun `report room, leave=true, report=true, report room failed`() = runTest {
val room = FakeBaseRoom(
roomId = roomId,
leaveRoomLambda = successLeaveRoomLambda,
reportRoomResult = failureReportRoomLambda
)
val client = FakeMatrixClient().apply {
givenGetRoomResult(roomId, room)
}
val reportRoom = DefaultReportRoom(client = client)
val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = true)
assertThat(result.isFailure).isTrue()
assertThat(result.exceptionOrNull()).isEqualTo(ReportRoom.Exception.ReportRoomFailed)
assert(successLeaveRoomLambda).isNeverCalled()
assert(failureReportRoomLambda).isCalledOnce()
}
}
@@ -0,0 +1,152 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.reportroom.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.reportroom.impl.fakes.FakeReportRoom
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.test.runTest
import org.junit.Test
class ReportRoomPresenterTest {
@Test
fun `present - initial state`() = runTest {
val presenter = createReportRoomPresenter()
presenter.test {
awaitItem().also { state ->
assertThat(state.reason).isEmpty()
assertThat(state.leaveRoom).isFalse()
assertThat(state.reportAction).isEqualTo(AsyncAction.Uninitialized)
assertThat(state.canReport).isFalse()
}
}
}
@Test
fun `present - update form values`() = runTest {
val presenter = createReportRoomPresenter()
presenter.test {
awaitItem().also { state ->
assertThat(state.reason).isEmpty()
assertThat(state.canReport).isFalse()
assertThat(state.leaveRoom).isFalse()
state.eventSink(ReportRoomEvents.UpdateReason("Spam"))
}
awaitItem().also { state ->
assertThat(state.reason).isEqualTo("Spam")
assertThat(state.canReport).isTrue()
assertThat(state.leaveRoom).isFalse()
state.eventSink(ReportRoomEvents.ToggleLeaveRoom)
}
awaitItem().also { state ->
assertThat(state.leaveRoom).isTrue()
assertThat(state.canReport).isTrue()
assertThat(state.canReport).isTrue()
}
}
}
@Test
fun `present - report room success`() = runTest {
val roomId = A_ROOM_ID
val reportRoomLambda = lambdaRecorder<RoomId, Boolean, String, Boolean, Result<Unit>> { _, _, _, _ -> Result.success(Unit) }
val reportRoom = FakeReportRoom(
lambda = reportRoomLambda
)
val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom)
presenter.test {
awaitItem().eventSink(ReportRoomEvents.ToggleLeaveRoom)
awaitItem().eventSink(ReportRoomEvents.Report)
awaitItem().also { state ->
assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java)
}
awaitItem().also { state ->
assertThat(state.reportAction).isInstanceOf(AsyncAction.Success::class.java)
}
assert(reportRoomLambda)
.isCalledOnce()
.with(value(roomId), value(true), any(), value(true))
}
}
@Test
fun `present - report failed`() = runTest {
val roomId = A_ROOM_ID
val reportRoomLambda = lambdaRecorder<RoomId, Boolean, String, Boolean, Result<Unit>> { _, _, _, _ ->
Result.failure(ReportRoom.Exception.ReportRoomFailed)
}
val reportRoom = FakeReportRoom(
lambda = reportRoomLambda
)
val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom)
presenter.test {
awaitItem().eventSink(ReportRoomEvents.Report)
awaitItem().also { state ->
assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java)
}
awaitItem().also { state ->
assertThat(state.reportAction).isInstanceOf(AsyncAction.Failure::class.java)
}
assert(reportRoomLambda)
.isCalledOnce()
.with(value(roomId), value(true), any(), any())
}
}
@Test
fun `present - leave room failed after report room success`() = runTest {
val roomId = A_ROOM_ID
val reportRoomLambda = lambdaRecorder<RoomId, Boolean, String, Boolean, Result<Unit>> { _, _, _, _ ->
Result.failure(ReportRoom.Exception.LeftRoomFailed)
}
val reportRoom = FakeReportRoom(
lambda = reportRoomLambda
)
val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom)
presenter.test {
awaitItem().eventSink(ReportRoomEvents.ToggleLeaveRoom)
awaitItem().eventSink(ReportRoomEvents.Report)
awaitItem().also { state ->
assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java)
}
awaitItem().also { state ->
assertThat(state.reportAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(ReportRoomEvents.Report)
}
awaitItem().also { state ->
assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java)
}
awaitItem().also { state ->
assertThat(state.reportAction).isInstanceOf(AsyncAction.Failure::class.java)
}
assert(reportRoomLambda)
.isCalledExactly(2)
.withSequence(
// The first call should report the room and try leaving it
listOf(value(roomId), value(true), any(), value(true)),
// The second call should not report the room again
listOf(value(roomId), value(false), any(), value(true))
)
}
}
}
internal fun createReportRoomPresenter(
roomId: RoomId = A_ROOM_ID,
reportRoom: ReportRoom = FakeReportRoom()
): ReportRoomPresenter {
return ReportRoomPresenter(roomId, reportRoom)
}
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.reportroom.impl
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTextInput
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.pressBack
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReportRoomViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on back invoke the expected callback`() {
val eventsRecorder = EventsRecorder<ReportRoomEvents>(expectEvents = false)
ensureCalledOnce {
rule.setReportRoomView(
aReportRoomState(
eventSink = eventsRecorder,
),
onBackClick = it
)
rule.pressBack()
}
}
@Test
fun `clicking on report when enabled emits the expected event`() {
val eventsRecorder = EventsRecorder<ReportRoomEvents>()
rule.setReportRoomView(
aReportRoomState(
reason = "Spam",
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_report)
eventsRecorder.assertSingle(ReportRoomEvents.Report)
}
@Test
fun `clicking on decline when disabled does not emit event`() {
val eventsRecorder = EventsRecorder<ReportRoomEvents>(expectEvents = false)
rule.setReportRoomView(
aReportRoomState(eventSink = eventsRecorder),
)
rule.clickOn(CommonStrings.action_report)
}
@Test
fun `clicking on leave room option emits the expected event`() {
val eventsRecorder = EventsRecorder<ReportRoomEvents>()
rule.setReportRoomView(
aReportRoomState(eventSink = eventsRecorder),
)
rule.clickOn(CommonStrings.action_leave_room)
eventsRecorder.assertSingle(ReportRoomEvents.ToggleLeaveRoom)
}
@Test
fun `typing text in the reason field emits the expected Event`() {
val eventsRecorder = EventsRecorder<ReportRoomEvents>()
rule.setReportRoomView(
aReportRoomState(
eventSink = eventsRecorder,
reason = ""
),
)
rule.onNodeWithText("").performTextInput("Spam!")
eventsRecorder.assertSingle(ReportRoomEvents.UpdateReason("Spam!"))
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setReportRoomView(
state: ReportRoomState,
onBackClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
ReportRoomView(
state = state,
onBackClick = onBackClick,
)
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.reportroom.impl.fakes
import io.element.android.features.reportroom.impl.ReportRoom
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
class FakeReportRoom(
var lambda: (RoomId, Boolean, String, Boolean) -> Result<Unit> = { _, _, _, _ -> lambdaError() }
) : ReportRoom {
override suspend fun invoke(
roomId: RoomId,
shouldReport: Boolean,
reason: String,
shouldLeave: Boolean
): Result<Unit> = simulateLongTask {
lambda(roomId, shouldReport, reason, shouldLeave)
}
}