From edd7130a97a5b69f83bce22726f35f5bbcae05ef Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Fri, 22 May 2026 17:56:32 +0200 Subject: [PATCH] Fix for overlapping timestamp when last block is not plainText (#5629) * fix for the overlapping timestamp when the last block is not plain text * remove id and use offset as iterator in the foearch of the components --- .../Other/Extensions/AttributedString.swift | 3 +-- .../AttributedStringBuilderProtocol.swift | 5 ++-- .../Timeline/View/Style/TimelineStyler.swift | 26 +++++++++++++++++++ .../TimelineItemViews/FormattedBodyText.swift | 17 ++++++++---- .../timelineItemStyler.Bubbles-iPad-en-GB.png | 4 +-- ...timelineItemStyler.Bubbles-iPad-pseudo.png | 4 +-- ...imelineItemStyler.Bubbles-iPhone-en-GB.png | 4 +-- ...melineItemStyler.Bubbles-iPhone-pseudo.png | 4 +-- 8 files changed, 49 insertions(+), 18 deletions(-) diff --git a/ElementX/Sources/Other/Extensions/AttributedString.swift b/ElementX/Sources/Other/Extensions/AttributedString.swift index 9e087cd84..40cb4d9bb 100644 --- a/ElementX/Sources/Other/Extensions/AttributedString.swift +++ b/ElementX/Sources/Other/Extensions/AttributedString.swift @@ -37,8 +37,7 @@ extension AttributedString { .plainText } - components.append(AttributedStringBuilderComponent(id: String(attributedString.characters), - attributedString: attributedString, + components.append(AttributedStringBuilderComponent(attributedString: attributedString, type: componentType)) } diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderProtocol.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderProtocol.swift index ff5c380ca..6b1c720b3 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderProtocol.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderProtocol.swift @@ -8,14 +8,13 @@ import Foundation -struct AttributedStringBuilderComponent: Hashable, Identifiable { +struct AttributedStringBuilderComponent: Hashable { enum ComponentType { case plainText case blockquote case codeBlock } - - let id: String + let attributedString: AttributedString let type: ComponentType } diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift index 2fe83923b..afb79af42 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift @@ -162,6 +162,30 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { sender: .test, content: .init(body: "😮")) + static let endingWithBlockquote: TextRoomTimelineItem = { + let builder = AttributedStringBuilder(cacheKey: "preview", mentionBuilder: MentionBuilder()) + let attributedString = builder.fromHTML("
Some text before
A quoted line at the end") + return TextRoomTimelineItem(id: .randomEvent, + timestamp: .mock, + isOutgoing: true, + isEditable: false, + canBeRepliedTo: true, + sender: .test, + content: .init(body: "", formattedBody: attributedString)) + }() + + static let endingWithCodeblock: TextRoomTimelineItem = { + let builder = AttributedStringBuilder(cacheKey: "preview", mentionBuilder: MentionBuilder()) + let attributedString = builder.fromHTML("
Some text before
let x = 42")
+ return TextRoomTimelineItem(id: .randomEvent,
+ timestamp: .mock,
+ isOutgoing: true,
+ isEditable: false,
+ canBeRepliedTo: true,
+ sender: .test,
+ content: .init(body: "", formattedBody: attributedString))
+ }()
+
static var testView: some View {
VStack(spacing: 0) {
TextRoomTimelineView(timelineItem: base)
@@ -170,6 +194,8 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
TextRoomTimelineView(timelineItem: sendingNonLast)
TextRoomTimelineView(timelineItem: sendingLast)
TextRoomTimelineView(timelineItem: failed)
+ TextRoomTimelineView(timelineItem: endingWithBlockquote)
+ TextRoomTimelineView(timelineItem: endingWithCodeblock)
}
}
diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift
index dce11e997..52b7f8b19 100644
--- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift
+++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift
@@ -63,11 +63,18 @@ struct FormattedBodyText: View {
// component so that the bubble's natural size accommodates the overlaid
// timestamp, and TextKit decides whether to keep the timestamp on the last
// line or push it to a new one.
- let components = attributedComponents
+ var components = attributedComponents
+ // When the body ends in a block component (blockquote/codeBlock) the overlaid
+ // timestamp would land on top of it, so append an empty trailing plain-text
+ // component that exists solely to reserve space for the timestamp underneath.
+ if trailingReservedSize != .zero, let last = components.last, last.type != .plainText {
+ components.append(AttributedStringBuilderComponent(attributedString: AttributedString(),
+ type: .plainText))
+ }
let lastPlainTextIndex = components.lastIndex { $0.type == .plainText }
-
+
return TimelineBubbleLayout(spacing: 8) {
- ForEach(Array(components.enumerated()), id: \.element.id) { index, component in
+ ForEach(Array(components.enumerated()), id: \.offset) { index, component in
switch component.type {
case .blockquote:
BlockquoteView(attributedString: component.attributedString, mode: .rendering)
@@ -88,10 +95,10 @@ struct FormattedBodyText: View {
.timelineBubbleLayoutSize(.natural)
}
}
-
+
// Make a second iteration through the components adding naturally sized versions of the
// block quotes and code blocks which are used for layout calculations but won't be rendered.
- ForEach(components) { component in
+ ForEach(Array(components.enumerated()), id: \.offset) { _, component in
switch component.type {
case .blockquote:
BlockquoteView(attributedString: component.attributedString, mode: .layout)
diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPad-en-GB.png
index 80050c38b..5201e85d5 100644
--- a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPad-en-GB.png
+++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPad-en-GB.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:67db610ecaf9e23c19dd893c6387e4b14fd71be69d31e921630aa65805fbd9c3
-size 92903
+oid sha256:c01ec03d81cbf209252a5b6d9ee50d18211f1fd7735f049b5fdb4c9f8425122e
+size 117158
diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPad-pseudo.png
index e6e8e423d..c72cff243 100644
--- a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPad-pseudo.png
+++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPad-pseudo.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2d6d16dff4f49fa7fd5011e3381d109448d4d2db631488007b9d6aa53e7ce95a
-size 98265
+oid sha256:3693e504ddecc2883790057b1f0c2ddd40170c5a2cd8ab522e27d361b8bf0a8e
+size 124281
diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPhone-en-GB.png
index 864f42ee5..625ddec73 100644
--- a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPhone-en-GB.png
+++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPhone-en-GB.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6576a0cb6536ffeb069ae6a777eb5fbbfb922066c5c18078373d76b57d86cc55
-size 50453
+oid sha256:ab7229d6a5f73bad5789ec03075170a25ae92ba020da68ca2b05014653c9732e
+size 71246
diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPhone-pseudo.png
index b87b19376..e534e1ddd 100644
--- a/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPhone-pseudo.png
+++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/timelineItemStyler.Bubbles-iPhone-pseudo.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3a1b607e90d93f167665a093df07ae75cbcc7e907628856cef79679322bdbac5
-size 54832
+oid sha256:a5d922f56785684a89937cc699c440349fd4ac6656052b67e000703ed36f24e9
+size 76911