[Message Actions] Report messages (#642)
* Add report messages feature * Try to improve how snackbars are delivered --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
committed by
GitHub
parent
898985cb9b
commit
bdb1841e44
@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
@@ -33,6 +34,7 @@ import com.bumble.appyx.core.plugin.NodeReadyObserver
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.utils.LocalSnackbarDispatcher
|
||||
import io.element.android.x.di.AppBindings
|
||||
import timber.log.Timber
|
||||
|
||||
@@ -42,11 +44,13 @@ class MainActivity : NodeComponentActivity() {
|
||||
|
||||
private lateinit var mainNode: MainNode
|
||||
|
||||
private lateinit var appBindings: AppBindings
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Timber.tag(loggerTag.value).w("onCreate, with savedInstanceState: ${savedInstanceState != null}")
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
val appBindings = bindings<AppBindings>()
|
||||
appBindings = bindings<AppBindings>()
|
||||
appBindings.matrixClientsHolder().restore(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
setContent {
|
||||
@@ -57,25 +61,29 @@ class MainActivity : NodeComponentActivity() {
|
||||
@Composable
|
||||
private fun MainContent(appBindings: AppBindings) {
|
||||
ElementTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
CompositionLocalProvider(
|
||||
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
|
||||
) {
|
||||
NodeHost(integrationPoint = appyxIntegrationPoint) {
|
||||
MainNode(
|
||||
it,
|
||||
appBindings.mainDaggerComponentOwner(),
|
||||
plugins = listOf(
|
||||
object : NodeReadyObserver<MainNode> {
|
||||
override fun init(node: MainNode) {
|
||||
Timber.tag(loggerTag.value).w("onMainNodeInit")
|
||||
mainNode = node
|
||||
mainNode.handleIntent(intent)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
) {
|
||||
NodeHost(integrationPoint = appyxIntegrationPoint) {
|
||||
MainNode(
|
||||
it,
|
||||
appBindings.mainDaggerComponentOwner(),
|
||||
plugins = listOf(
|
||||
object : NodeReadyObserver<MainNode> {
|
||||
override fun init(node: MainNode) {
|
||||
Timber.tag(loggerTag.value).w("onMainNodeInit")
|
||||
mainNode = node
|
||||
mainNode.handleIntent(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ package io.element.android.x.di
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import io.element.android.appnav.di.MatrixClientsHolder
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
interface AppBindings {
|
||||
fun matrixClientsHolder(): MatrixClientsHolder
|
||||
fun mainDaggerComponentOwner(): MainDaggerComponentsOwner
|
||||
fun snackbarDispatcher(): SnackbarDispatcher
|
||||
}
|
||||
|
||||
1
changelog.d/489.feature
Normal file
1
changelog.d/489.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add option to report inappropriate content
|
||||
@@ -35,12 +35,14 @@ import io.element.android.features.messages.impl.attachments.preview.Attachments
|
||||
import io.element.android.features.messages.impl.forward.ForwardMessagesNode
|
||||
import io.element.android.features.messages.impl.media.local.MediaInfo
|
||||
import io.element.android.features.messages.impl.media.viewer.MediaViewerNode
|
||||
import io.element.android.features.messages.impl.report.ReportMessageNode
|
||||
import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
@@ -83,6 +85,9 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
|
||||
@Parcelize
|
||||
data class ForwardEvent(val eventId: EventId) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data class ReportMessage(val eventId: EventId, val senderId: UserId) : NavTarget
|
||||
}
|
||||
|
||||
private val callback = plugins<MessagesEntryPoint.Callback>().firstOrNull()
|
||||
@@ -114,6 +119,10 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
override fun onForwardEventClicked(eventId: EventId) {
|
||||
backstack.push(NavTarget.ForwardEvent(eventId))
|
||||
}
|
||||
|
||||
override fun onReportMessage(eventId: EventId, senderId: UserId) {
|
||||
backstack.push(NavTarget.ReportMessage(eventId, senderId))
|
||||
}
|
||||
}
|
||||
createNode<MessagesNode>(buildContext, listOf(callback))
|
||||
}
|
||||
@@ -142,6 +151,10 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
}
|
||||
createNode<ForwardMessagesNode>(buildContext, listOf(inputs, callback))
|
||||
}
|
||||
is NavTarget.ReportMessage -> {
|
||||
val inputs = ReportMessageNode.Inputs(navTarget.eventId, navTarget.senderId)
|
||||
createNode<ReportMessageNode>(buildContext, listOf(inputs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +210,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
Children(
|
||||
navModel = backstack,
|
||||
modifier = modifier,
|
||||
transitionHandler = rememberDefaultTransitionHandler(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
package io.element.android.features.messages.impl
|
||||
|
||||
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.timeline.item.TimelineItemDebugInfo
|
||||
|
||||
interface MessagesNavigator {
|
||||
fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo)
|
||||
fun onForwardEventClicked(eventId: EventId)
|
||||
fun onReportContentClicked(eventId: EventId, senderId: UserId)
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ class MessagesNode @AssistedInject constructor(
|
||||
fun onUserDataClicked(userId: UserId)
|
||||
fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo)
|
||||
fun onForwardEventClicked(eventId: EventId)
|
||||
fun onReportMessage(eventId: EventId, senderId: UserId)
|
||||
}
|
||||
|
||||
private fun onRoomDetailsClicked() {
|
||||
@@ -75,6 +76,10 @@ class MessagesNode @AssistedInject constructor(
|
||||
callback?.onForwardEventClicked(eventId)
|
||||
}
|
||||
|
||||
override fun onReportContentClicked(eventId: EventId, senderId: UserId) {
|
||||
callback?.onReportMessage(eventId, senderId)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
|
||||
@@ -161,7 +161,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||
TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState)
|
||||
TimelineItemAction.Developer -> handleShowDebugInfoAction(targetEvent)
|
||||
TimelineItemAction.Forward -> handleForwardAction(targetEvent)
|
||||
TimelineItemAction.ReportContent -> notImplementedYet()
|
||||
TimelineItemAction.ReportContent -> handleReportAction(targetEvent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,4 +241,9 @@ class MessagesPresenter @AssistedInject constructor(
|
||||
if (event.eventId == null) return
|
||||
navigator.onForwardEventClicked(event.eventId)
|
||||
}
|
||||
|
||||
private fun handleReportAction(event: TimelineItem.Event) {
|
||||
if (event.eventId == null) return
|
||||
navigator.onReportContentClicked(event.eventId, event.senderId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
/*
|
||||
* 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 @@
|
||||
/*
|
||||
* 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 @@
|
||||
/*
|
||||
* 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 @@
|
||||
/*
|
||||
* 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 @@
|
||||
/*
|
||||
* 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,
|
||||
)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package io.element.android.features.messages
|
||||
|
||||
import io.element.android.features.messages.impl.MessagesNavigator
|
||||
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.timeline.item.TimelineItemDebugInfo
|
||||
|
||||
class FakeMessagesNavigator : MessagesNavigator {
|
||||
@@ -27,6 +28,9 @@ class FakeMessagesNavigator : MessagesNavigator {
|
||||
var onForwardEventClickedCount = 0
|
||||
private set
|
||||
|
||||
var onReportContentClickedCount = 0
|
||||
private set
|
||||
|
||||
override fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) {
|
||||
onShowEventDebugInfoClickedCount++
|
||||
}
|
||||
@@ -34,4 +38,8 @@ class FakeMessagesNavigator : MessagesNavigator {
|
||||
override fun onForwardEventClicked(eventId: EventId) {
|
||||
onForwardEventClickedCount++
|
||||
}
|
||||
|
||||
override fun onReportContentClicked(eventId: EventId, senderId: UserId) {
|
||||
onReportContentClickedCount++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +284,8 @@ class MessagesPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - handle action report content`() = runTest {
|
||||
val presenter = createMessagePresenter()
|
||||
val navigator = FakeMessagesNavigator()
|
||||
val presenter = createMessagePresenter(navigator = navigator)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -292,6 +293,7 @@ class MessagesPresenterTest {
|
||||
val initialState = awaitItem()
|
||||
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent()))
|
||||
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
|
||||
assertThat(navigator.onReportContentClickedCount).isEqualTo(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,8 @@ class MediaViewerPresenterTest {
|
||||
fun `present - check all actions `() = runTest {
|
||||
val mediaLoader = FakeMediaLoader()
|
||||
val mediaActions = FakeLocalMediaActions()
|
||||
val presenter = aMediaViewerPresenter(mediaLoader, mediaActions)
|
||||
val snackbarDispatcher = SnackbarDispatcher()
|
||||
val presenter = aMediaViewerPresenter(mediaLoader, mediaActions, snackbarDispatcher)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
@@ -90,26 +91,24 @@ class MediaViewerPresenterTest {
|
||||
state.eventSink(MediaViewerEvents.SaveOnDisk)
|
||||
state = awaitItem()
|
||||
assertThat(state.snackbarMessage).isNotNull()
|
||||
state = awaitItem()
|
||||
assertThat(state.snackbarMessage).isNull()
|
||||
snackbarDispatcher.clear()
|
||||
assertThat(awaitItem().snackbarMessage).isNull()
|
||||
|
||||
// Check failures
|
||||
mediaActions.shouldFail = true
|
||||
state.eventSink(MediaViewerEvents.OpenWith)
|
||||
state = awaitItem()
|
||||
assertThat(state.snackbarMessage).isNotNull()
|
||||
state = awaitItem()
|
||||
assertThat(state.snackbarMessage).isNull()
|
||||
snackbarDispatcher.clear()
|
||||
assertThat(awaitItem().snackbarMessage).isNull()
|
||||
state.eventSink(MediaViewerEvents.Share)
|
||||
state = awaitItem()
|
||||
assertThat(state.snackbarMessage).isNotNull()
|
||||
state = awaitItem()
|
||||
assertThat(state.snackbarMessage).isNull()
|
||||
snackbarDispatcher.clear()
|
||||
assertThat(awaitItem().snackbarMessage).isNull()
|
||||
state.eventSink(MediaViewerEvents.SaveOnDisk)
|
||||
state = awaitItem()
|
||||
assertThat(state.snackbarMessage).isNotNull()
|
||||
state = awaitItem()
|
||||
assertThat(state.snackbarMessage).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +144,7 @@ class MediaViewerPresenterTest {
|
||||
private fun aMediaViewerPresenter(
|
||||
mediaLoader: FakeMediaLoader,
|
||||
localMediaActions: FakeLocalMediaActions,
|
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||
): MediaViewerPresenter {
|
||||
return MediaViewerPresenter(
|
||||
inputs = MediaViewerNode.Inputs(
|
||||
@@ -155,7 +155,7 @@ class MediaViewerPresenterTest {
|
||||
localMediaFactory = localMediaFactory,
|
||||
mediaLoader = mediaLoader,
|
||||
localMediaActions = localMediaActions,
|
||||
snackbarDispatcher = SnackbarDispatcher()
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -22,13 +22,14 @@ import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
@@ -52,19 +53,16 @@ class SnackbarDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
/** Used to provide a [SnackbarDispatcher] to composable functions, it's needed for [rememberSnackbarHostState]. */
|
||||
val LocalSnackbarDispatcher = compositionLocalOf<SnackbarDispatcher> {
|
||||
error("No SnackbarDispatcher provided")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun handleSnackbarMessage(
|
||||
snackbarDispatcher: SnackbarDispatcher
|
||||
): SnackbarMessage? {
|
||||
val snackbarMessage by snackbarDispatcher.snackbarMessage.collectAsState(initial = null)
|
||||
LaunchedEffect(snackbarMessage) {
|
||||
if (snackbarMessage != null) {
|
||||
launch {
|
||||
snackbarDispatcher.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
return snackbarMessage
|
||||
return snackbarDispatcher.snackbarMessage.collectAsState(initial = null).value
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -74,6 +72,7 @@ fun rememberSnackbarHostState(snackbarMessage: SnackbarMessage?): SnackbarHostSt
|
||||
val snackbarMessageText = snackbarMessage?.let {
|
||||
stringResource(id = snackbarMessage.messageResId)
|
||||
}
|
||||
val dispatcher = LocalSnackbarDispatcher.current
|
||||
LaunchedEffect(snackbarMessage) {
|
||||
if (snackbarMessageText == null) return@LaunchedEffect
|
||||
coroutineScope.launch {
|
||||
@@ -81,6 +80,9 @@ fun rememberSnackbarHostState(snackbarMessage: SnackbarMessage?): SnackbarHostSt
|
||||
message = snackbarMessageText,
|
||||
duration = snackbarMessage.duration,
|
||||
)
|
||||
if (isActive) {
|
||||
dispatcher.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
return snackbarHostState
|
||||
|
||||
@@ -112,4 +112,6 @@ interface MatrixRoom : Closeable {
|
||||
suspend fun setName(name: String): Result<Unit>
|
||||
|
||||
suspend fun setTopic(topic: String): Result<Unit>
|
||||
|
||||
suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit>
|
||||
}
|
||||
|
||||
@@ -329,4 +329,13 @@ class RustMatrixRoom(
|
||||
innerRoom.setTopic(topic)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit> = withContext(coroutineDispatchers.io) {
|
||||
runCatching {
|
||||
innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason)
|
||||
if (blockUserId != null) {
|
||||
innerRoom.ignoreUser(blockUserId.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ class FakeMatrixRoom(
|
||||
private var retrySendMessageResult = Result.success(Unit)
|
||||
private var cancelSendResult = Result.success(Unit)
|
||||
private var forwardEventResult = Result.success(Unit)
|
||||
private var reportContentResult = Result.success(Unit)
|
||||
|
||||
var sendMediaCount = 0
|
||||
private set
|
||||
@@ -89,6 +90,9 @@ class FakeMatrixRoom(
|
||||
var cancelSendCount: Int = 0
|
||||
private set
|
||||
|
||||
var reportedContentCount: Int = 0
|
||||
private set
|
||||
|
||||
var isInviteAccepted: Boolean = false
|
||||
private set
|
||||
|
||||
@@ -249,6 +253,15 @@ class FakeMatrixRoom(
|
||||
setTopicResult
|
||||
}
|
||||
|
||||
override suspend fun reportContent(
|
||||
eventId: EventId,
|
||||
reason: String,
|
||||
blockUserId: UserId?
|
||||
): Result<Unit> = simulateLongTask {
|
||||
reportedContentCount++
|
||||
return reportContentResult
|
||||
}
|
||||
|
||||
override fun close() = Unit
|
||||
|
||||
fun givenLeaveRoomError(throwable: Throwable?) {
|
||||
@@ -338,4 +351,8 @@ class FakeMatrixRoom(
|
||||
fun givenForwardEventResult(result: Result<Unit>) {
|
||||
forwardEventResult = result
|
||||
}
|
||||
|
||||
fun givenReportContentResult(result: Result<Unit>) {
|
||||
reportContentResult = result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ android {
|
||||
dependencies {
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.test.parameter.injector)
|
||||
testImplementation(projects.libraries.designsystem)
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
ksp(libs.showkase.processor)
|
||||
kspTest(libs.showkase.processor)
|
||||
|
||||
@@ -39,6 +39,8 @@ import com.android.ide.common.rendering.api.SessionParams
|
||||
import com.google.testing.junit.testparameterinjector.TestParameter
|
||||
import com.google.testing.junit.testparameterinjector.TestParameterInjector
|
||||
import io.element.android.libraries.designsystem.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.utils.LocalSnackbarDispatcher
|
||||
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -96,6 +98,8 @@ class ScreenshotTest {
|
||||
LocalConfiguration provides Configuration().apply {
|
||||
setLocales(LocaleList(localeStr.toLocale()))
|
||||
},
|
||||
// Needed to display Snackbars and avoid crashes during screenshot tests
|
||||
LocalSnackbarDispatcher provides SnackbarDispatcher(),
|
||||
// Needed so that UI that uses it don't crash during screenshot tests
|
||||
LocalOnBackPressedDispatcherOwner provides object : OnBackPressedDispatcherOwner {
|
||||
override val lifecycle: Lifecycle get() = lifecycleOwner.lifecycle
|
||||
|
||||
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.
@@ -113,7 +113,8 @@ Compose:
|
||||
CompositionLocalAllowlist:
|
||||
active: true
|
||||
# You can optionally define a list of CompositionLocals that are allowed here
|
||||
allowedCompositionLocals: LocalColors, LocalCompoundColors
|
||||
|
||||
allowedCompositionLocals: LocalColors, LocalCompoundColors, LocalSnackbarDispatcher
|
||||
CompositionLocalNaming:
|
||||
active: true
|
||||
ContentEmitterReturningValues:
|
||||
|
||||
Reference in New Issue
Block a user