From c0bb68c872476a0a81027920418dc9cf93d58a43 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 17 Sep 2026 19:08:32 +0200 Subject: [PATCH] New timeline event renderer: add new modules (#7721) * Added HTML parser to abstract tree nodes * Add HTML renderer using `HtmlMessageContent` * Add custom rendering for inline code nodes besides the monospace font * Support `` and `` too * Add `ContentAvoidingLayoutData` support * Improve the last block node behaviour and its integration with `ContentAvoidingLayout` * Move `ContentAvoidingLayout` to `:libraries:ui-common` so it can be shared * Add missing `:libraries:htmlrenderer:impl` entry in `DependencyHandlerScope.allLibrariesImpl` * Update screenshots --------- Co-authored-by: ElementBot --- features/messages/impl/build.gradle.kts | 1 + .../pinned/list/PinnedMessagesListView.kt | 2 +- .../components/TimelineItemEventRow.kt | 4 +- .../TimelineItemGroupedEventsRow.kt | 2 +- .../timeline/components/TimelineItemRow.kt | 2 +- .../event/TimelineItemAttachmentView.kt | 4 +- .../components/event/TimelineItemAudioView.kt | 2 +- .../event/TimelineItemEncryptedView.kt | 2 +- .../event/TimelineItemEventContentView.kt | 4 +- .../components/event/TimelineItemFileView.kt | 2 +- .../event/TimelineItemInformativeView.kt | 2 +- .../event/TimelineItemRedactedView.kt | 2 +- .../components/event/TimelineItemTextView.kt | 4 +- .../event/TimelineItemUnknownView.kt | 2 +- .../components/event/TimelineItemVoiceView.kt | 2 +- libraries/htmlrenderer/api/build.gradle.kts | 19 + .../htmlrenderer/api/HtmlMessageParser.kt | 40 ++ .../htmlrenderer/api/MentionNodeContent.kt | 42 ++ .../libraries/htmlrenderer/api/MessageNode.kt | 95 +++++ libraries/htmlrenderer/impl/build.gradle.kts | 36 ++ .../impl/DefaultHtmlMessageParser.kt | 237 ++++++++++++ ...ontentAvoidingHtmlMessageContentPreview.kt | 70 ++++ .../impl/renderer/HtmlMessageContent.kt | 362 ++++++++++++++++++ .../renderer/HtmlMessageContentPreview.kt | 212 ++++++++++ .../impl/DefaultHtmlMessageParserTest.kt | 204 ++++++++++ libraries/htmlrenderer/test/build.gradle.kts | 18 + .../test/FakeHtmlMessageParser.kt | 19 + libraries/ui-common/build.gradle.kts | 1 + .../common}/layout/ContentAvoidingLayout.kt | 19 +- .../kotlin/extension/DependencyHandleScope.kt | 1 + .../tests/konsist/KonsistClassNameTest.kt | 1 + ...eeditor_AttachmentImageEditorView_2_en.png | 4 +- ...eeditor_AttachmentImageEditorView_5_en.png | 4 +- ...essagesViewWithIdentityChange_Day_0_en.png | 4 +- ...essagesViewWithIdentityChange_Day_1_en.png | 4 +- ...essagesViewWithIdentityChange_Day_2_en.png | 4 +- ...sagesViewWithIdentityChange_Night_0_en.png | 4 +- ...sagesViewWithIdentityChange_Night_1_en.png | 4 +- ...sagesViewWithIdentityChange_Night_2_en.png | 4 +- ...veVerifiedUserSendFailureView_Day_1_en.png | 4 +- ...veVerifiedUserSendFailureView_Day_2_en.png | 4 +- ...VerifiedUserSendFailureView_Night_1_en.png | 4 +- ...VerifiedUserSendFailureView_Night_2_en.png | 4 +- ...s.event_ATimelineItemEventRow_Day_0_en.png | 4 +- ...event_ATimelineItemEventRow_Night_0_en.png | 4 +- ...lineItemEventRowDisambiguated_Day_0_en.png | 4 +- ...neItemEventRowDisambiguated_Night_0_en.png | 4 +- ...mEventRowWithReplyInformative_Day_0_en.png | 4 +- ...mEventRowWithReplyInformative_Day_1_en.png | 4 +- ...ventRowWithReplyInformative_Night_0_en.png | 4 +- ...ventRowWithReplyInformative_Night_1_en.png | 4 +- ...ineItemEventRowWithReplyOther_Day_0_en.png | 4 +- ...ineItemEventRowWithReplyOther_Day_1_en.png | 4 +- ...eItemEventRowWithReplyOther_Night_0_en.png | 4 +- ...eItemEventRowWithReplyOther_Night_1_en.png | 4 +- ...TimelineItemEventRowWithReply_Day_0_en.png | 4 +- ...imelineItemEventRowWithReply_Day_10_en.png | 4 +- ...imelineItemEventRowWithReply_Day_11_en.png | 4 +- ...TimelineItemEventRowWithReply_Day_1_en.png | 4 +- ...TimelineItemEventRowWithReply_Day_2_en.png | 4 +- ...TimelineItemEventRowWithReply_Day_3_en.png | 4 +- ...TimelineItemEventRowWithReply_Day_4_en.png | 4 +- ...TimelineItemEventRowWithReply_Day_5_en.png | 4 +- ...TimelineItemEventRowWithReply_Day_6_en.png | 4 +- ...TimelineItemEventRowWithReply_Day_7_en.png | 4 +- ...TimelineItemEventRowWithReply_Day_8_en.png | 4 +- ...TimelineItemEventRowWithReply_Day_9_en.png | 4 +- ...melineItemEventRowWithReply_Night_0_en.png | 4 +- ...elineItemEventRowWithReply_Night_10_en.png | 4 +- ...elineItemEventRowWithReply_Night_11_en.png | 4 +- ...melineItemEventRowWithReply_Night_1_en.png | 4 +- ...melineItemEventRowWithReply_Night_2_en.png | 4 +- ...melineItemEventRowWithReply_Night_3_en.png | 4 +- ...melineItemEventRowWithReply_Night_4_en.png | 4 +- ...melineItemEventRowWithReply_Night_5_en.png | 4 +- ...melineItemEventRowWithReply_Night_6_en.png | 4 +- ...melineItemEventRowWithReply_Night_7_en.png | 4 +- ...melineItemEventRowWithReply_Night_8_en.png | 4 +- ...melineItemEventRowWithReply_Night_9_en.png | 4 +- ...s.messages.impl_MessagesView_Day_12_en.png | 4 +- ....messages.impl_MessagesView_Night_0_en.png | 4 +- ...messages.impl_MessagesView_Night_10_en.png | 4 +- ...messages.impl_MessagesView_Night_12_en.png | 4 +- ....messages.impl_MessagesView_Night_2_en.png | 4 +- ....messages.impl_MessagesView_Night_4_en.png | 4 +- ....messages.impl_MessagesView_Night_6_en.png | 4 +- ....messages.impl_MessagesView_Night_8_en.png | 4 +- ...blockedusers_BlockedUsersView_Day_4_en.png | 4 +- ...blockedusers_BlockedUsersView_Day_5_en.png | 4 +- ...ockedusers_BlockedUsersView_Night_4_en.png | 4 +- ...ockedusers_BlockedUsersView_Night_5_en.png | 4 +- ...mpl.userstatus_UserStatusView_Day_5_en.png | 4 +- ...mpl.userstatus_UserStatusView_Day_6_en.png | 4 +- ...l.userstatus_UserStatusView_Night_5_en.png | 4 +- ...l.userstatus_UserStatusView_Night_6_en.png | 4 +- ...impl_RoomMemberModerationView_Day_5_en.png | 4 +- ...impl_RoomMemberModerationView_Day_7_en.png | 4 +- ...pl_RoomMemberModerationView_Night_5_en.png | 4 +- ...pl_RoomMemberModerationView_Night_7_en.png | 4 +- ...orizontalFloatingToolbarNoFab_Day_0_en.png | 4 +- ...izontalFloatingToolbarNoFab_Night_0_en.png | 4 +- ...nts_HorizontalFloatingToolbar_Day_0_en.png | 4 +- ...s_HorizontalFloatingToolbar_Night_0_en.png | 4 +- ...l.renderer_HtmlMessageContent_Day_0_en.png | 3 + ...l.renderer_HtmlMessageContent_Day_1_en.png | 3 + ...l.renderer_HtmlMessageContent_Day_2_en.png | 3 + ...l.renderer_HtmlMessageContent_Day_3_en.png | 3 + ...l.renderer_HtmlMessageContent_Day_4_en.png | 3 + ...l.renderer_HtmlMessageContent_Day_5_en.png | 3 + ...l.renderer_HtmlMessageContent_Day_6_en.png | 3 + ...l.renderer_HtmlMessageContent_Day_7_en.png | 3 + ...l.renderer_HtmlMessageContent_Day_8_en.png | 3 + ...l.renderer_HtmlMessageContent_Day_9_en.png | 3 + ...renderer_HtmlMessageContent_Night_0_en.png | 3 + ...renderer_HtmlMessageContent_Night_1_en.png | 3 + ...renderer_HtmlMessageContent_Night_2_en.png | 3 + ...renderer_HtmlMessageContent_Night_3_en.png | 3 + ...renderer_HtmlMessageContent_Night_4_en.png | 3 + ...renderer_HtmlMessageContent_Night_5_en.png | 3 + ...renderer_HtmlMessageContent_Night_6_en.png | 3 + ...renderer_HtmlMessageContent_Night_7_en.png | 3 + ...renderer_HtmlMessageContent_Night_8_en.png | 3 + ...renderer_HtmlMessageContent_Night_9_en.png | 3 + 123 files changed, 1592 insertions(+), 169 deletions(-) create mode 100644 libraries/htmlrenderer/api/build.gradle.kts create mode 100644 libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/HtmlMessageParser.kt create mode 100644 libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/MentionNodeContent.kt create mode 100644 libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/MessageNode.kt create mode 100644 libraries/htmlrenderer/impl/build.gradle.kts create mode 100644 libraries/htmlrenderer/impl/src/main/kotlin/io/element/android/libraries/htmlrenderer/impl/DefaultHtmlMessageParser.kt create mode 100644 libraries/htmlrenderer/impl/src/main/kotlin/io/element/android/libraries/htmlrenderer/impl/renderer/ContentAvoidingHtmlMessageContentPreview.kt create mode 100644 libraries/htmlrenderer/impl/src/main/kotlin/io/element/android/libraries/htmlrenderer/impl/renderer/HtmlMessageContent.kt create mode 100644 libraries/htmlrenderer/impl/src/main/kotlin/io/element/android/libraries/htmlrenderer/impl/renderer/HtmlMessageContentPreview.kt create mode 100644 libraries/htmlrenderer/impl/src/test/kotlin/io/element/android/libraries/htmlrenderer/impl/DefaultHtmlMessageParserTest.kt create mode 100644 libraries/htmlrenderer/test/build.gradle.kts create mode 100644 libraries/htmlrenderer/test/src/main/kotlin/io/element/android/libraries/htmlrenderer/test/FakeHtmlMessageParser.kt rename {features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components => libraries/ui-common/src/main/kotlin/io/element/android/libraries/ui/common}/layout/ContentAvoidingLayout.kt (94%) create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Day_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Day_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Day_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Day_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Day_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Day_7_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Day_8_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Day_9_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Night_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Night_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Night_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Night_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Night_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Night_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Night_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Night_7_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Night_8_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.htmlrenderer.impl.renderer_HtmlMessageContent_Night_9_en.png diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 0b51a2b80d..58c71a96f6 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { implementation(projects.libraries.voicerecorder.api) implementation(projects.libraries.mediaplayer.api) implementation(projects.libraries.push.api) + implementation(projects.libraries.uiCommon) implementation(projects.libraries.uiUtils) implementation(projects.libraries.testtags) implementation(projects.libraries.wellknown.api) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt index 5daec3e275..298a783e97 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListView.kt @@ -36,7 +36,6 @@ import io.element.android.features.messages.impl.timeline.components.TimelineIte import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvent import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryEvent import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryView import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -58,6 +57,7 @@ import io.element.android.libraries.emoji.api.picker.EmojiPickerRenderer import io.element.android.libraries.emoji.api.picker.NoOpEmojiPickerRenderer import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.analytics.compose.LocalAnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 99e72f4973..97ec71173a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -72,8 +72,6 @@ import io.element.android.features.messages.impl.timeline.TimelineEvent import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -139,6 +137,8 @@ import io.element.android.libraries.matrix.ui.messages.sender.SenderName import io.element.android.libraries.matrix.ui.messages.sender.SenderNameMode import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayout +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData import io.element.android.libraries.ui.strings.CommonPlurals import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.utils.a11y.isTalkbackActive diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt index 5b620a7dc3..0128e80b79 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemGroupedEventsRow.kt @@ -23,7 +23,6 @@ import io.element.android.features.messages.impl.timeline.aRedactedMessagesGroup import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.components.receipt.ReadReceiptViewState import io.element.android.features.messages.impl.timeline.components.receipt.TimelineItemReadReceiptView import io.element.android.features.messages.impl.timeline.groups.isRedactedMessagesGroup @@ -35,6 +34,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData import io.element.android.wysiwyg.link.Link @Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index fb9659ff70..aa66c262b5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -29,7 +29,6 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.timeline.TimelineEvent import io.element.android.features.messages.impl.timeline.TimelineRoomInfo import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.RtcNotificationState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLegacyCallInviteContent @@ -46,6 +45,7 @@ import io.element.android.libraries.designsystem.text.toPx import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.wysiwyg.link.Link diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentView.kt index 38d575718f..586ff6cfe5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAttachmentView.kt @@ -22,12 +22,12 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayout +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData /** * package-private, you should only use TimelineItemFileView and TimelineItemAudioView. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt index d47ca552b8..a6c6afcbc9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt @@ -12,11 +12,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContentPreviewParam import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData @Composable fun TimelineItemAudioView( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt index 6b9022d3cf..38920af287 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEncryptedView.kt @@ -12,7 +12,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContentPreviewParam import io.element.android.libraries.designsystem.icons.CompoundDrawables @@ -20,6 +19,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UtdCause +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData import io.element.android.libraries.ui.strings.CommonStrings @Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt index 4b7ab9372d..839df1d320 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemEventContentView.kt @@ -24,8 +24,6 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.features.messages.impl.timeline.TimelineEvent -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories import io.element.android.features.messages.impl.timeline.di.rememberPresenter import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAttachmentsContent @@ -60,6 +58,8 @@ import io.element.android.libraries.matrix.ui.media.contentvalidation.NotFoundCo import io.element.android.libraries.matrix.ui.media.contentvalidation.collectOverallState import io.element.android.libraries.matrix.ui.media.contentvalidation.rememberEventContentValidationState import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayout +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData import io.element.android.libraries.voiceplayer.api.VoiceMessageState import io.element.android.wysiwyg.compose.EditorStyledText import io.element.android.wysiwyg.link.Link diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt index 77df9ba923..bc3fe91eca 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt @@ -13,11 +13,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContentPreviewParam import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData import io.element.android.libraries.ui.strings.CommonStrings /** diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemInformativeView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemInformativeView.kt index 41a950ee5e..34f22d9d48 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemInformativeView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemInformativeView.kt @@ -22,12 +22,12 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData @Composable fun TimelineItemInformativeView( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemRedactedView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemRedactedView.kt index e32de7f4ae..70ac84eb31 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemRedactedView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemRedactedView.kt @@ -11,11 +11,11 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData import io.element.android.libraries.ui.strings.CommonStrings @Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt index 86b50f3015..bb8e30aca3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt @@ -23,8 +23,6 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.compound.theme.ElementTheme -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayout -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.AN_EMOJI_ONLY_TEXT import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContentPreviewParam @@ -36,6 +34,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.utils.LocalUiTestMode import io.element.android.libraries.textcomposer.ElementRichTextEditorStyle import io.element.android.libraries.textcomposer.mentions.LocalMentionSpanUpdater +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayout +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData import io.element.android.wysiwyg.compose.EditorStyledText import io.element.android.wysiwyg.link.Link diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemUnknownView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemUnknownView.kt index fbbefca207..8f375ddb5b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemUnknownView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemUnknownView.kt @@ -11,11 +11,11 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.libraries.designsystem.icons.CompoundDrawables import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData import io.element.android.libraries.ui.strings.CommonStrings @Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt index a05ac78f84..55bc5866d6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVoiceView.kt @@ -40,7 +40,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.messages.impl.timeline.components.layout.ContentAvoidingLayoutData import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContentPreviewParam import io.element.android.libraries.designsystem.atomic.atoms.PlaybackSpeedButton @@ -52,6 +51,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.ui.media.contentvalidation.ContentValidationValue +import io.element.android.libraries.ui.common.layout.ContentAvoidingLayoutData import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.utils.a11y.isTalkbackActive import io.element.android.libraries.voiceplayer.api.VoiceMessageEvent diff --git a/libraries/htmlrenderer/api/build.gradle.kts b/libraries/htmlrenderer/api/build.gradle.kts new file mode 100644 index 0000000000..a475be7eb0 --- /dev/null +++ b/libraries/htmlrenderer/api/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +plugins { + id("io.element.android-compose-library") +} + +android { + namespace = "io.element.android.libraries.htmlrenderer.api" +} + +dependencies { + api(projects.libraries.matrix.api) + api(libs.jsoup) +} diff --git a/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/HtmlMessageParser.kt b/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/HtmlMessageParser.kt new file mode 100644 index 0000000000..f0c3f67249 --- /dev/null +++ b/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/HtmlMessageParser.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.htmlrenderer.api + +import org.jsoup.nodes.Document + +/** + * Parses the HTML [Document] of a formatted message body into a tree of [MessageNode]s + * that a Compose renderer can turn into composables. + * + * The [Document] is expected to have already been sanitized (e.g. via + * `FormattedBody.toHtmlDocument`) so that only supported tags remain. + */ +interface HtmlMessageParser { + /** + * Converts [document] into a [DocumentNode]. Never throws: unknown or empty content + * yields a [DocumentNode] with no children. + */ + fun parse(document: Document): DocumentNode + + companion object { + /** + * Tag used for the string annotation attached to link (``) text ranges in a + * [ParagraphNode.text]. The annotation value is the link target URL. + */ + const val LINK_ANNOTATION_TAG = "url" + + /** + * Tag used for the string annotation attached to inline code (``) text ranges in a + * [ParagraphNode.text]. The annotation value is empty; only the range matters. A `` + * nested inside a `
` is part of the [CodeBlockNode] and is never annotated this way.
+         */
+        const val INLINE_CODE_ANNOTATION_TAG = "inline_code"
+    }
+}
diff --git a/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/MentionNodeContent.kt b/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/MentionNodeContent.kt
new file mode 100644
index 0000000000..4886dcef23
--- /dev/null
+++ b/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/MentionNodeContent.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2026 Element Creations Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.htmlrenderer.api
+
+import androidx.compose.runtime.Immutable
+import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
+import io.element.android.libraries.matrix.api.core.UserId
+
+/**
+ * Describes a mention "pill" referenced from a [ParagraphNode].
+ *
+ * Each mention occupies an inline-content placeholder inside the paragraph's
+ * [ParagraphNode.text]; the renderer looks the id up in [ParagraphNode.inlineContent]
+ * and draws the corresponding pill.
+ */
+@Immutable
+sealed interface MentionNodeContent {
+    /** The text shown inside the pill (e.g. a display name, prefixed with `@` for users). */
+    val displayText: String
+
+    /** A mention of a specific user (``). */
+    data class User(
+        override val displayText: String,
+        val userId: UserId,
+    ) : MentionNodeContent
+
+    /** A mention of a room (``). */
+    data class Room(
+        override val displayText: String,
+        val roomIdOrAlias: RoomIdOrAlias,
+    ) : MentionNodeContent
+
+    /** An `@room` mention notifying everyone in the room. */
+    data class Everyone(
+        override val displayText: String,
+    ) : MentionNodeContent
+}
diff --git a/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/MessageNode.kt b/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/MessageNode.kt
new file mode 100644
index 0000000000..05be7fd8ea
--- /dev/null
+++ b/libraries/htmlrenderer/api/src/main/kotlin/io/element/android/libraries/htmlrenderer/api/MessageNode.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2026 Element Creations Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+package io.element.android.libraries.htmlrenderer.api
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.text.AnnotatedString
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.ImmutableMap
+import kotlinx.collections.immutable.persistentMapOf
+
+/**
+ * A node in the parsed representation of a formatted (HTML) message body.
+ *
+ * The tree is produced by [HtmlMessageParser] and consumed by a Compose renderer.
+ * Block-level nodes ([BlockNode]) either contain other block nodes (nested lists, a
+ * quote containing paragraphs, ...) or terminate in a [ParagraphNode] / [CodeBlockNode]
+ * that carries the actual text.
+ */
+@Immutable
+sealed interface MessageNode
+
+/** A block-level node, laid out vertically by the renderer. */
+@Immutable
+sealed interface BlockNode : MessageNode
+
+/**
+ * The root of a parsed message: an ordered list of block-level children.
+ */
+@Immutable
+data class DocumentNode(
+    val children: ImmutableList,
+) : BlockNode
+
+/**
+ * A run of inline content (text with inline formatting) already flattened to an
+ * [AnnotatedString]. This is the only leaf that carries styled text.
+ *
+ * Inline formatting (``, ``, ``, ``, inline ``) is encoded as
+ * [androidx.compose.ui.text.SpanStyle] ranges, and links (``) as string annotations
+ * tagged [HtmlMessageParser.LINK_ANNOTATION_TAG].
+ *
+ * Mentions (pills) are encoded as inline-content placeholders inside [text] (see
+ * [androidx.compose.foundation.text.appendInlineContent]); [inlineContent] maps each
+ * placeholder id to the [MentionNodeContent] the renderer needs to draw the pill.
+ */
+@Immutable
+data class ParagraphNode(
+    val text: AnnotatedString,
+    val inlineContent: ImmutableMap = persistentMapOf(),
+) : BlockNode
+
+/**
+ * A preformatted code block (`
`). Rendered in a monospaced, horizontally
+ * scrollable container. The text is kept raw: no inline styling is applied inside it.
+ */
+@Immutable
+data class CodeBlockNode(
+    val code: String,
+) : BlockNode
+
+/**
+ * A block quote (`
`). Quotes can nest and can contain any block content. + */ +@Immutable +data class QuoteNode( + val children: ImmutableList, +) : BlockNode + +/** + * An ordered (`
    `) or unordered (`