diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index e8a51cca22..010035e1f7 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -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 = bindings() 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 { - 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 { + override fun init(node: MainNode) { + Timber.tag(loggerTag.value).w("onMainNodeInit") + mainNode = node + mainNode.handleIntent(intent) + } } - } + ) ) - ) + } } } } diff --git a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt index 9d5fa9446f..59f7e98d20 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt @@ -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 } diff --git a/changelog.d/489.feature b/changelog.d/489.feature new file mode 100644 index 0000000000..4ffd2b7a4a --- /dev/null +++ b/changelog.d/489.feature @@ -0,0 +1 @@ +Add option to report inappropriate content diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index e74c55395f..901716451f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -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().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(buildContext, listOf(callback)) } @@ -142,6 +151,10 @@ class MessagesFlowNode @AssistedInject constructor( } createNode(buildContext, listOf(inputs, callback)) } + is NavTarget.ReportMessage -> { + val inputs = ReportMessageNode.Inputs(navTarget.eventId, navTarget.senderId) + createNode(buildContext, listOf(inputs)) + } } } @@ -197,6 +210,7 @@ class MessagesFlowNode @AssistedInject constructor( Children( navModel = backstack, modifier = modifier, + transitionHandler = rememberDefaultTransitionHandler(), ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt index d4733da047..201173a0bf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt @@ -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) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 8624b62e75..651ae8670b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -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() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 6b64dc8bdd..e305b916cd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -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) + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageEvents.kt new file mode 100644 index 0000000000..ed5ee029e7 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageEvents.kt @@ -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 +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageNode.kt new file mode 100644 index 0000000000..1be4571161 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageNode.kt @@ -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, + presenterFactory: ReportMessagePresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val eventId: EventId, + val senderId: UserId, + ) : NodeInputs + + private val 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 + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt new file mode 100644 index 0000000000..09cad8e33b --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt @@ -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 { + + 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> = 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>, + ) = launch { + suspend { + val userIdToBlock = userId.takeIf { blockUser } + room.reportContent(eventId, reason, userIdToBlock) + .onSuccess { + snackbarDispatcher.post(SnackbarMessage(StringR.string.common_report_submitted)) + } + }.executeResult(result) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageState.kt new file mode 100644 index 0000000000..809668c88f --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageState.kt @@ -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, + val eventSink: (ReportMessageEvents) -> Unit +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt new file mode 100644 index 0000000000..89e6d7a220 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt @@ -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 { + override val values: Sequence + 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 = Async.Uninitialized, +) = ReportMessageState( + reason = reason, + blockUser = blockUser, + result = result, + eventSink = {} +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageView.kt new file mode 100644 index 0000000000..1beee54519 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageView.kt @@ -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, + ) +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt index d9dd135eca..8a374e5bcb 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt @@ -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++ + } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 375af5b8cd..40af424406 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -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) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt index 8a6025d3bd..2b66d2cf9b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt @@ -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, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt new file mode 100644 index 0000000000..090dd36dbe --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt @@ -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, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt index 35b81ff324..1de46c78e0 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt @@ -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 { + 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 diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 9e8f8514b7..afd0e8ea25 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -112,4 +112,6 @@ interface MatrixRoom : Closeable { suspend fun setName(name: String): Result suspend fun setTopic(topic: String): Result + + suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index c1adbc0cb9..bc8525d87b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -329,4 +329,13 @@ class RustMatrixRoom( innerRoom.setTopic(topic) } } + + override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result = withContext(coroutineDispatchers.io) { + runCatching { + innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason) + if (blockUserId != null) { + innerRoom.ignoreUser(blockUserId.value) + } + } + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 5b641b8883..d8187b0a1d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -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 = simulateLongTask { + reportedContentCount++ + return reportContentResult + } + override fun close() = Unit fun givenLeaveRoomError(throwable: Throwable?) { @@ -338,4 +351,8 @@ class FakeMatrixRoom( fun givenForwardEventResult(result: Result) { forwardEventResult = result } + + fun givenReportContentResult(result: Result) { + reportContentResult = result + } } diff --git a/tests/uitests/build.gradle.kts b/tests/uitests/build.gradle.kts index ed4f5298ce..13f43b22d4 100644 --- a/tests/uitests/build.gradle.kts +++ b/tests/uitests/build.gradle.kts @@ -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) diff --git a/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/ScreenshotTest.kt b/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/ScreenshotTest.kt index f7afb1e4a8..a929f7d182 100644 --- a/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/ScreenshotTest.kt +++ b/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/ScreenshotTest.kt @@ -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 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..34557a047e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c78c731d2a100cd5632d3018a0a2f9ee3c4ecdd69feebb233f8fb60efb29808a +size 44647 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7756aef065 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a88c76ee497bc9544dda80517959d35862708ec25539aa910cfcf94d55dc17e +size 46580 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a37d6a00cb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c54a2b29fc3584b3de6e5d11b87200bae6ea9ce371c7382297496f0022110da5 +size 46232 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e6ba2b331c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c11907197357eaced1a3ac9dec28ada97ab871d5662ce270d0af000204ebdf11 +size 43878 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..feace46eb5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a6b210c88398341ab21982e485f08c4503198fdc550a077ffea4f123f5513e0 +size 35451 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b89bf3802a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18ec14976369b542ce426d9b559fe4d72faa8db8e7402884be20dca670106ade +size 43921 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d74122f832 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:539ed1ad5366e2757a99c5589ea6052bb47cc494805164c0a17f8e4970f0b102 +size 45084 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ac7d452d59 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e788d5011a1635273949be92a350a7e4068831935dfae328e5699fc361a9cc6b +size 44662 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4c84f916e9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:396fa2ca9b01b467fb5160d776130eb6b40351a93d4ed9cc4e61a027c977cac0 +size 42258 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..566e949537 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cb0fb381934d4548d10f3b6654ecf04bc0fa29266ed58fa5653dafd322eade0 +size 34503 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index dc797aace1..f5be110ffa 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -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: