From 688ab3bd5e2a38925268807b05c2fca2f2d90e47 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 9 Jun 2023 08:13:22 +0200 Subject: [PATCH] [Message Actions] Display debug info for events in the timeline (#555) * Display debug info for events in the timeline on debug builds. --------- Co-authored-by: ElementBot --- build.gradle.kts | 4 +- changelog.d/554.feature | 1 + .../messages/impl/MessagesFlowNode.kt | 14 ++ .../features/messages/impl/MessagesNode.kt | 8 + .../messages/impl/MessagesPresenter.kt | 2 +- .../features/messages/impl/MessagesView.kt | 11 +- .../impl/timeline/TimelineStateProvider.kt | 11 + .../impl/timeline/debug/EventDebugInfoNode.kt | 60 ++++++ .../impl/timeline/debug/EventDebugInfoView.kt | 193 ++++++++++++++++++ .../event/TimelineItemEventFactory.kt | 1 + .../impl/timeline/model/TimelineItem.kt | 2 + .../actionlist/ActionListPresenterTest.kt | 29 +-- .../messages/fixtures/aMessageEvent.kt | 4 + .../groups/TimelineItemGrouperTest.kt | 2 + .../timeline/item/TimelineItemDebugInfo.kt | 27 +++ .../timeline/item/event/EventTimelineItem.kt | 4 +- .../item/event/EventTimelineItemMapper.kt | 13 +- .../matrix/test/room/RoomSummaryFixture.kt | 13 +- ...ViewPreviewDark_0_null,NEXUS_5,1.0,en].png | 3 + ...iewPreviewLight_0_null,NEXUS_5,1.0,en].png | 3 + 20 files changed, 371 insertions(+), 34 deletions(-) create mode 100644 changelog.d/554.feature create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/TimelineItemDebugInfo.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewDark_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewLight_0_null,NEXUS_5,1.0,en].png diff --git a/build.gradle.kts b/build.gradle.kts index ef55a63eba..4ff0ad0fc9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -208,11 +208,11 @@ koverMerged { name = "Global minimum code coverage." target = kotlinx.kover.api.VerificationTarget.ALL bound { - minValue = 50 + minValue = 55 // Setting a max value, so that if coverage is bigger, it means that we have to change minValue. // For instance if we have minValue = 20 and maxValue = 30, and current code coverage is now 31.32%, update // minValue to 25 and maxValue to 35. - maxValue = 60 + maxValue = 65 counter = kotlinx.kover.api.CounterType.INSTRUCTION valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE } diff --git a/changelog.d/554.feature b/changelog.d/554.feature new file mode 100644 index 0000000000..3d64b6fe57 --- /dev/null +++ b/changelog.d/554.feature @@ -0,0 +1 @@ +Created debug info screen for events in the timeline, it can be used only in debug builds. 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 6fe5a6beed..ec4d2f31d3 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 @@ -34,6 +34,7 @@ import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode 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.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 @@ -41,8 +42,10 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.createNode 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 import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import kotlinx.collections.immutable.ImmutableList import kotlinx.parcelize.Parcelize @@ -72,6 +75,9 @@ class MessagesFlowNode @AssistedInject constructor( @Parcelize data class AttachmentPreview(val attachment: Attachment) : NavTarget + + @Parcelize + data class EventDebugInfo(val eventId: EventId, val debugInfo: TimelineItemDebugInfo) : NavTarget } private val callback = plugins().firstOrNull() @@ -95,6 +101,10 @@ class MessagesFlowNode @AssistedInject constructor( override fun onUserDataClicked(userId: UserId) { callback?.onUserDataClicked(userId) } + + override fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) { + backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo)) + } } createNode(buildContext, listOf(callback)) } @@ -110,6 +120,10 @@ class MessagesFlowNode @AssistedInject constructor( val inputs = AttachmentsPreviewNode.Inputs(navTarget.attachment) createNode(buildContext, listOf(inputs)) } + is NavTarget.EventDebugInfo -> { + val inputs = EventDebugInfoNode.Inputs(navTarget.eventId, navTarget.debugInfo) + createNode(buildContext, listOf(inputs)) + } } } 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 96f76d3a34..58d73a10f7 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 @@ -28,7 +28,9 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.timeline.model.TimelineItem 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 +import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import kotlinx.collections.immutable.ImmutableList @ContributesNode(RoomScope::class) @@ -45,6 +47,7 @@ class MessagesNode @AssistedInject constructor( fun onEventClicked(event: TimelineItem.Event) fun onPreviewAttachments(attachments: ImmutableList) fun onUserDataClicked(userId: UserId) + fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) } private fun onRoomDetailsClicked() { @@ -63,6 +66,10 @@ class MessagesNode @AssistedInject constructor( callback?.onUserDataClicked(userId) } + private fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) { + callback?.onShowEventDebugInfoClicked(eventId, debugInfo) + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -73,6 +80,7 @@ class MessagesNode @AssistedInject constructor( onEventClicked = this::onEventClicked, onPreviewAttachments = this::onPreviewAttachments, onUserDataClicked = this::onUserDataClicked, + onItemDebugInfoClicked = this::onShowEventDebugInfoClicked, modifier = modifier, ) } 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 015e76f448..932e89087d 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 @@ -129,7 +129,7 @@ class MessagesPresenter @Inject constructor( TimelineItemAction.Redact -> handleActionRedact(targetEvent) TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState) TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState) - TimelineItemAction.Developer -> notImplementedYet() + TimelineItemAction.Developer -> Unit // Handled at UI level TimelineItemAction.ReportContent -> notImplementedYet() } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 05ee38a2ec..b2bc8afce3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -77,7 +77,9 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState +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 import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch import timber.log.Timber @@ -92,6 +94,7 @@ fun MessagesView( onEventClicked: (event: TimelineItem.Event) -> Unit, onUserDataClicked: (UserId) -> Unit, onPreviewAttachments: (ImmutableList) -> Unit, + onItemDebugInfoClicked: (EventId, TimelineItemDebugInfo) -> Unit, modifier: Modifier = Modifier, ) { LogCompositions(tag = "MessagesScreen", msg = "Root") @@ -120,7 +123,12 @@ fun MessagesView( fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) { isMessageActionsBottomSheetVisible = false - state.eventSink(MessagesEvents.HandleAction(action, event)) + when (action) { + is TimelineItemAction.Developer -> if (event.eventId != null && event.debugInfo != null) { + onItemDebugInfoClicked(event.eventId, event.debugInfo) + } + else -> state.eventSink(MessagesEvents.HandleAction(action, event)) + } } fun onDismissActionListBottomSheet() { @@ -275,5 +283,6 @@ private fun ContentToPreview(state: MessagesState) { onEventClicked = {}, onPreviewAttachments = {}, onUserDataClicked = {}, + onItemDebugInfoClicked = { _, _ -> }, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 7e23657974..47e09fc577 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData 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.MatrixTimeline +import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import kotlinx.collections.immutable.ImmutableList @@ -98,6 +99,7 @@ internal fun aTimelineItemEvent( groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, sendState: EventSendState = EventSendState.Sent(eventId), inReplyTo: InReplyTo? = null, + debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), ): TimelineItem.Event { return TimelineItem.Event( id = eventId.value, @@ -116,5 +118,14 @@ internal fun aTimelineItemEvent( groupPosition = groupPosition, sendState = sendState, inReplyTo = inReplyTo, + debugInfo = debugInfo, ) } + +internal fun aTimelineItemDebugInfo( + model: String = "Rust(Model())", + originalJson: String? = null, + latestEditedJson: String? = null, +) = TimelineItemDebugInfo( + model, originalJson, latestEditedJson +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.kt new file mode 100644 index 0000000000..faa80d134b --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoNode.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.timeline.debug + +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.timeline.item.TimelineItemDebugInfo + +@ContributesNode(RoomScope::class) +class EventDebugInfoNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val eventId: EventId, + val timelineItemDebugInfo: TimelineItemDebugInfo, + ) : NodeInputs + + private val inputs = inputs() + + private fun onBackPressed() { + navigateUp() + } + + @Composable + override fun View(modifier: Modifier) = with(inputs) { + EventDebugInfoView( + eventId = eventId, + model = timelineItemDebugInfo.model, + originalJson = timelineItemDebugInfo.originalJson, + latestEditedJson = timelineItemDebugInfo.latestEditedJson, + onBackPressed = ::onBackPressed + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt new file mode 100644 index 0000000000..f3fc79e0a3 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/debug/EventDebugInfoView.kt @@ -0,0 +1,193 @@ +/* + * 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.timeline.debug + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowDropUp +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.getSystemService +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.core.EventId + +/** + * Screen used to display debug info for events. + * It will only be available in debug builds. + */ +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun EventDebugInfoView( + eventId: EventId, + model: String, + originalJson: String?, + latestEditedJson: String?, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, + isTest: Boolean = false, +) { + val sectionsInitiallyExpanded = isTest || LocalInspectionMode.current + Scaffold( + topBar = { + TopAppBar( + title = { + Text("Debug event info") + }, + navigationIcon = { BackButton(onClick = onBackPressed) } + ) + }, + modifier = modifier + ) { padding -> + LazyColumn(modifier = Modifier + .fillMaxWidth() + .padding(padding) // Window insets + .consumeWindowInsets(padding) + .padding(horizontal = 16.dp) // Internal padding + ) { + item { + Column(Modifier.padding(vertical = 10.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text(text = "Event ID:") + CopyableText(text = eventId.value) + } + } + item { + CollapsibleSection(title = "Model:", text = model, initiallyExpanded = sectionsInitiallyExpanded) + } + if (originalJson != null) { + item { + CollapsibleSection(title = "Original JSON:", text = originalJson, initiallyExpanded = sectionsInitiallyExpanded) + } + } + if (latestEditedJson != null) { + item { + CollapsibleSection(title = "Latest edited JSON:", text = latestEditedJson, initiallyExpanded = sectionsInitiallyExpanded) + } + } + } + } +} + +@Composable +private fun CollapsibleSection( + title: String, + text: String, + modifier: Modifier = Modifier, + initiallyExpanded: Boolean = false, +) { + var isExpanded by remember { mutableStateOf(initiallyExpanded) } + Column(modifier = modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .clickable { isExpanded = !isExpanded } + .fillMaxWidth() + .padding(vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(title, modifier = Modifier.weight(1f)) + Icon( + imageVector = if (isExpanded) Icons.Filled.ArrowDropUp else Icons.Filled.ArrowDropDown, + contentDescription = null + ) + } + AnimatedVisibility(visible = isExpanded, enter = expandVertically(), exit = shrinkVertically()) { + CopyableText(text = text) + } + } +} + +@Composable +private fun CopyableText( + text: String, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val clipboardManager = remember { requireNotNull(context.getSystemService()) } + Box( + modifier + .clip(RoundedCornerShape(4.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(6.dp) + .clickable { clipboardManager.setPrimaryClip(ClipData.newPlainText("JSON", text)) } + ) { + Text(text = text, fontFamily = FontFamily.Monospace, fontSize = 14.sp, modifier = Modifier.padding(8.dp)) + } +} + +@Preview +@Composable +internal fun EventDebugInfoViewPreviewLight() { + ElementPreviewLight { + ContentToPreview() + } +} + +@Preview +@Composable +internal fun EventDebugInfoViewPreviewDark() { + ElementPreviewDark { + ContentToPreview() + } +} + +@Composable +private fun ContentToPreview() { + EventDebugInfoView( + eventId = EventId("\$some-event-id"), + model = "Rust(\n\tModel()\n)", + originalJson = "{\"name\": \"original\"}", + latestEditedJson = "{\"name\": \"edited\"}", + onBackPressed = { } + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index ecd85a213a..ce9a558b97 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -82,6 +82,7 @@ class TimelineItemEventFactory @Inject constructor( reactionsState = currentTimelineItem.computeReactionsState(), sendState = currentTimelineItem.event.localSendState ?: EventSendState.NotSentYet, inReplyTo = currentTimelineItem.event.inReplyTo(), + debugInfo = currentTimelineItem.event.debugInfo, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index 5a5f382a54..0328bf6603 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.timeline.model.virtual.Timeline import io.element.android.libraries.designsystem.components.avatar.AvatarData 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 import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import kotlinx.collections.immutable.ImmutableList @@ -61,6 +62,7 @@ sealed interface TimelineItem { val reactionsState: TimelineItemReactions, val sendState: EventSendState, val inReplyTo: InReplyTo?, + val debugInfo: TimelineItemDebugInfo, ) : TimelineItem { val showSenderInformation = groupPosition.isNew() && !isMine diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt index b1aa411148..7af17d193f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt @@ -20,23 +20,16 @@ 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.fixtures.aMessageEvent import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction -import io.element.android.features.messages.impl.timeline.model.TimelineItem -import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType -import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState -import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE -import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.A_USER_NAME import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.test.runTest import org.junit.Test @@ -60,7 +53,7 @@ class ActionListPresenterTest { presenter.present() }.test { val initialState = awaitItem() - val messageEvent = aMessageEvent(true, TimelineItemRedactedContent) + val messageEvent = aMessageEvent(isMine = true, content = TimelineItemRedactedContent) initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) @@ -87,7 +80,7 @@ class ActionListPresenterTest { presenter.present() }.test { val initialState = awaitItem() - val messageEvent = aMessageEvent(false, TimelineItemRedactedContent) + val messageEvent = aMessageEvent(isMine = false, content = TimelineItemRedactedContent) initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent)) // val loadingState = awaitItem() // assertThat(loadingState.target).isEqualTo(ActionListState.Target.Loading(messageEvent)) @@ -232,19 +225,3 @@ private fun aBuildMeta( private fun anActionListPresenter(isBuildDebuggable: Boolean) = ActionListPresenter(buildMeta = aBuildMeta(isDebuggable = isBuildDebuggable)) -private fun aMessageEvent( - isMine: Boolean, - content: TimelineItemEventContent, -) = TimelineItem.Event( - id = AN_EVENT_ID.value, - eventId = AN_EVENT_ID, - senderId = A_USER_ID, - senderDisplayName = A_USER_NAME, - senderAvatar = AvatarData(A_USER_ID.value, A_USER_NAME), - content = content, - sentTime = "", - isMine = isMine, - reactionsState = TimelineItemReactions(persistentListOf()), - sendState = EventSendState.Sent(AN_EVENT_ID), - inReplyTo = null, -) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt index 496c022983..943c98504e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt @@ -22,12 +22,14 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.matrix.test.room.aTimelineItemDebugInfo import kotlinx.collections.immutable.persistentListOf internal fun aMessageEvent( @@ -35,6 +37,7 @@ internal fun aMessageEvent( isMine: Boolean = true, content: TimelineItemEventContent = TimelineItemTextContent(body = A_MESSAGE, htmlDocument = null, isEdited = false), inReplyTo: InReplyTo? = null, + debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), ) = TimelineItem.Event( id = eventId?.value.orEmpty(), eventId = eventId, @@ -47,4 +50,5 @@ internal fun aMessageEvent( reactionsState = TimelineItemReactions(persistentListOf()), sendState = EventSendState.Sent(AN_EVENT_ID), inReplyTo = inReplyTo, + debugInfo = debugInfo, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt index b1a17beb6c..309efddcac 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventSendStat import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.room.aTimelineItemDebugInfo import kotlinx.collections.immutable.toImmutableList import org.junit.Test @@ -44,6 +45,7 @@ class TimelineItemGrouperTest { reactionsState = TimelineItemReactions(emptyList().toImmutableList()), sendState = EventSendState.Sent(AN_EVENT_ID), inReplyTo = null, + debugInfo = aTimelineItemDebugInfo(), ) private val aNonGroupableItem = aMessageEvent() private val aNonGroupableItemNoEvent = TimelineItem.Virtual("virtual", aTimelineItemDaySeparatorModel("Today")) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/TimelineItemDebugInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/TimelineItemDebugInfo.kt new file mode 100644 index 0000000000..b7292ff907 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/TimelineItemDebugInfo.kt @@ -0,0 +1,27 @@ +/* + * 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.libraries.matrix.api.timeline.item + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class TimelineItemDebugInfo( + val model: String, + val originalJson: String?, + val latestEditedJson: String?, +) : Parcelable diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt index 81aa2dc5c4..05f440c413 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.api.timeline.item.event 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 data class EventTimelineItem( val uniqueIdentifier: String, @@ -31,7 +32,8 @@ data class EventTimelineItem( val sender: UserId, val senderProfile: ProfileTimelineDetails, val timestamp: Long, - val content: EventContent + val content: EventContent, + val debugInfo: TimelineItemDebugInfo, ) { fun inReplyTo(): InReplyTo? { return (content as? MessageContent)?.inReplyTo diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index 669466b9cf..a77ddbd80f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl.timeline.item.event 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 import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem @@ -25,6 +26,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimeli import org.matrix.rustcomponents.sdk.Reaction import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem +import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo import org.matrix.rustcomponents.sdk.ProfileDetails as RustProfileDetails class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper()) { @@ -42,7 +44,8 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap sender = UserId(it.sender()), senderProfile = it.senderProfile().map(), timestamp = it.timestamp().toLong(), - content = contentMapper.map(it.content()) + content = contentMapper.map(it.content()), + debugInfo = it.debugInfo().map(), ) } } @@ -77,3 +80,11 @@ private fun List?.map(): List { ) } ?: emptyList() } + +private fun RustEventTimelineItemDebugInfo.map(): TimelineItemDebugInfo { + return TimelineItemDebugInfo( + model = model, + originalJson = originalJson, + latestEditedJson = latestEditJson, + ) +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index 960d2c3974..e6ac93a3ab 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -22,15 +22,14 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.room.RoomSummaryDetails import io.element.android.libraries.matrix.api.room.message.RoomMessage +import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem -import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import io.element.android.libraries.matrix.test.AN_EVENT_ID -import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_UNIQUE_ID @@ -100,6 +99,7 @@ fun anEventTimelineItem( senderProfile: ProfileTimelineDetails = aProfileTimelineDetails(), timestamp: Long = 0L, content: EventContent = aProfileChangeMessageContent(), + debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), ) = EventTimelineItem( uniqueIdentifier = uniqueIdentifier, eventId = eventId, @@ -113,6 +113,7 @@ fun anEventTimelineItem( senderProfile = senderProfile, timestamp = timestamp, content = content, + debugInfo = debugInfo, ) fun aProfileTimelineDetails( @@ -136,3 +137,11 @@ fun aProfileChangeMessageContent( avatarUrl = avatarUrl, prevAvatarUrl = prevAvatarUrl, ) + +fun aTimelineItemDebugInfo( + model: String = "Rust(Model())", + originalJson: String? = null, + latestEditedJson: String? = null, +) = TimelineItemDebugInfo( + model, originalJson, latestEditedJson +) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewDark_0_null,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.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewDark_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ee1fb0bfc5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewDark_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43777d9d5cd310d53ea4ae8c05f86a96fd18540d46af5ee05b707f28ca1bd74b +size 35561 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewLight_0_null,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.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewLight_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..49bd2e9e5d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.debug_null_DefaultGroup_EventDebugInfoViewPreviewLight_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a11c22f8f593d6ae7dd297dc67b89898cdf0f4b4036b16625fa6f9c5043a28e5 +size 34689