update all the timeline item views to use the new implemementation with nstextattachment

This commit is contained in:
Mauro Romito
2026-05-11 17:11:18 +02:00
committed by Mauro
parent 686753721e
commit 2217803825
12 changed files with 57 additions and 69 deletions
@@ -17,7 +17,7 @@ struct AudioMediaEventsTimelineView: View {
fileSize: timelineItem.content.fileSize,
caption: timelineItem.content.caption,
formattedCaption: timelineItem.content.formattedCaption,
additionalWhitespaces: timelineItem.additionalWhitespaces(),
trailingReservedSize: timelineItem.trailingReservedSize,
isAudioFile: true)
.accessibilityLabel(L10n.commonAudio)
.frame(maxWidth: .infinity, alignment: .leading)
@@ -17,7 +17,7 @@ struct FileMediaEventsTimelineView: View {
fileSize: timelineItem.content.fileSize,
caption: timelineItem.content.caption,
formattedCaption: timelineItem.content.formattedCaption,
additionalWhitespaces: timelineItem.additionalWhitespaces())
trailingReservedSize: timelineItem.trailingReservedSize)
.accessibilityLabel(L10n.commonFile)
.frame(maxWidth: .infinity, alignment: .leading)
.bubbleBackground(isOutgoing: timelineItem.isOutgoing)
@@ -19,7 +19,7 @@ struct AudioRoomTimelineView: View {
fileSize: timelineItem.content.fileSize,
caption: timelineItem.content.caption,
formattedCaption: timelineItem.content.formattedCaption,
additionalWhitespaces: timelineItem.additionalWhitespaces(),
trailingReservedSize: timelineItem.trailingReservedSize,
shouldBoost: timelineItem.shouldBoost,
isAudioFile: true) {
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
@@ -15,9 +15,11 @@ struct EmoteRoomTimelineView: View, TextBasedRoomTimelineViewProtocol {
var body: some View {
TimelineStyler(timelineItem: timelineItem) {
if let attributedString = timelineItem.content.formattedBody {
FormattedBodyText(attributedString: attributedString, additionalWhitespacesCount: timelineItem.additionalWhitespaces())
FormattedBodyText(attributedString: attributedString,
trailingReservedSize: timelineItem.trailingReservedSize)
} else {
FormattedBodyText(text: timelineItem.content.body, additionalWhitespacesCount: timelineItem.additionalWhitespaces())
FormattedBodyText(text: timelineItem.content.body,
trailingReservedSize: timelineItem.trailingReservedSize)
}
}
}
@@ -19,7 +19,7 @@ struct FileRoomTimelineView: View {
fileSize: timelineItem.content.fileSize,
caption: timelineItem.content.caption,
formattedCaption: timelineItem.content.formattedCaption,
additionalWhitespaces: timelineItem.additionalWhitespaces(),
trailingReservedSize: timelineItem.trailingReservedSize,
shouldBoost: timelineItem.shouldBoost) {
context?.send(viewAction: .mediaTapped(itemID: timelineItem.id))
}
@@ -35,7 +35,7 @@ struct MediaFileRoomTimelineContent: View {
let fileSize: UInt?
let caption: String?
let formattedCaption: AttributedString?
let additionalWhitespaces: Int
var trailingReservedSize: CGSize = .zero
var shouldBoost = false
var isAudioFile = false
@@ -66,11 +66,11 @@ struct MediaFileRoomTimelineContent: View {
if let formattedCaption {
FormattedBodyText(attributedString: formattedCaption,
additionalWhitespacesCount: additionalWhitespaces,
trailingReservedSize: trailingReservedSize,
boostFontSize: shouldBoost)
} else if let caption {
FormattedBodyText(text: caption,
additionalWhitespacesCount: additionalWhitespaces,
trailingReservedSize: trailingReservedSize,
boostFontSize: shouldBoost)
}
}
@@ -12,7 +12,7 @@ struct FormattedBodyText: View {
@Environment(\.layoutDirection) private var layoutDirection
private let attributedString: AttributedString
private let additionalWhitespacesCount: Int
private let trailingReservedSize: CGSize
private let boostFontSize: Bool
private let defaultAttributesContainer: AttributeContainer = {
@@ -22,14 +22,9 @@ struct FormattedBodyText: View {
container.foregroundColor = UIColor.compound.textPrimary
return container
}()
private var attributedComponents: [AttributedStringBuilderComponent] {
var adjustedAttributedString = attributedString + AttributedString(additionalWhitespacesSuffix)
// If this is not a list, force the writing direction by adding the correct unicode character.
if !String(attributedString.characters).starts(with: "\t") {
adjustedAttributedString = AttributedString(layoutDirection.isolateLayoutUnicodeString) + adjustedAttributedString
}
var adjustedAttributedString = attributedString
// Required to allow the underlying TextView to use body font when no font is specifie in the AttributedString.
adjustedAttributedString.mergeAttributes(defaultAttributesContainer, mergePolicy: .keepCurrent)
@@ -44,24 +39,19 @@ struct FormattedBodyText: View {
}
init(attributedString: AttributedString,
additionalWhitespacesCount: Int = 0,
trailingReservedSize: CGSize = .zero,
boostFontSize: Bool = false) {
self.attributedString = attributedString
self.additionalWhitespacesCount = additionalWhitespacesCount
self.trailingReservedSize = trailingReservedSize
self.boostFontSize = boostFontSize
}
init(text: String, additionalWhitespacesCount: Int = 0, boostFontSize: Bool = false) {
init(text: String, trailingReservedSize: CGSize = .zero, boostFontSize: Bool = false) {
self.init(attributedString: AttributedString(text),
additionalWhitespacesCount: additionalWhitespacesCount,
trailingReservedSize: trailingReservedSize,
boostFontSize: boostFontSize)
}
/// These is needed to create the slightly off inlined timestamp effect
private var additionalWhitespacesSuffix: String {
.generateBreakableWhitespaceEnd(whitespaceCount: additionalWhitespacesCount, layoutDirection: layoutDirection)
}
var body: some View {
layout
.tint(.compound.textLinkExternal)
@@ -71,36 +61,39 @@ struct FormattedBodyText: View {
/// The attributed components laid out for the bubbles timeline style.
var layout: some View {
TimelineBubbleLayout(spacing: 8) {
ForEach(attributedComponents) { component in
// Ignore if the string contains only the layout correction
if String(component.attributedString.characters) == layoutDirection.isolateLayoutUnicodeString {
EmptyView()
} else {
switch component.type {
case .blockquote:
BlockquoteView(attributedString: component.attributedString, mode: .rendering)
.timelineBubbleLayoutSize(.bubbleWidth(mode: .rendering))
case .codeBlock:
CodeBlockView(attributedString: component.attributedString, mode: .rendering)
.timelineBubbleLayoutSize(.bubbleWidth(mode: .rendering))
.contextMenu {
Button(L10n.actionCopy) {
UIPasteboard.general.string = component.attributedString.string
}
// The trailing reserved area must only be applied to the last plain text
// 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
let lastPlainTextIndex = components.lastIndex { $0.type == .plainText }
return TimelineBubbleLayout(spacing: 8) {
ForEach(Array(components.enumerated()), id: \.element.id) { index, component in
switch component.type {
case .blockquote:
BlockquoteView(attributedString: component.attributedString, mode: .rendering)
.timelineBubbleLayoutSize(.bubbleWidth(mode: .rendering))
case .codeBlock:
CodeBlockView(attributedString: component.attributedString, mode: .rendering)
.timelineBubbleLayoutSize(.bubbleWidth(mode: .rendering))
.contextMenu {
Button(L10n.actionCopy) {
UIPasteboard.general.string = component.attributedString.string
}
case .plainText:
MessageText(attributedString: component.attributedString)
.padding(.horizontal, 4)
.fixedSize(horizontal: false, vertical: true)
.timelineBubbleLayoutSize(.natural)
}
}
case .plainText:
MessageText(attributedString: component.attributedString,
trailingReservedSize: index == lastPlainTextIndex ? trailingReservedSize : .zero)
.padding(.horizontal, 4)
.fixedSize(horizontal: false, vertical: true)
.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(attributedComponents) { component in
ForEach(components) { component in
switch component.type {
case .blockquote:
BlockquoteView(attributedString: component.attributedString, mode: .layout)
@@ -32,11 +32,11 @@ struct ImageRoomTimelineView: View {
if let attributedCaption = timelineItem.content.formattedCaption {
FormattedBodyText(attributedString: attributedCaption,
additionalWhitespacesCount: timelineItem.additionalWhitespaces(),
trailingReservedSize: timelineItem.trailingReservedSize,
boostFontSize: timelineItem.shouldBoost)
} else if let caption = timelineItem.content.caption {
FormattedBodyText(text: caption,
additionalWhitespacesCount: timelineItem.additionalWhitespaces(),
trailingReservedSize: timelineItem.trailingReservedSize,
boostFontSize: timelineItem.shouldBoost)
}
}
@@ -37,7 +37,8 @@ struct LocationRoomTimelineView: View {
.aspectRatio(mapAspectRatio, contentMode: .fit)
.clipped()
} else {
FormattedBodyText(text: timelineItem.body, additionalWhitespacesCount: timelineItem.additionalWhitespaces())
FormattedBodyText(text: timelineItem.body,
trailingReservedSize: timelineItem.trailingReservedSize)
}
}
@@ -18,9 +18,11 @@ struct NoticeRoomTimelineView: View, TextBasedRoomTimelineViewProtocol {
Label {
if let attributedString = timelineItem.content.formattedBody {
FormattedBodyText(attributedString: attributedString, additionalWhitespacesCount: timelineItem.additionalWhitespaces())
FormattedBodyText(attributedString: attributedString,
trailingReservedSize: timelineItem.trailingReservedSize)
} else {
FormattedBodyText(text: timelineItem.content.body, additionalWhitespacesCount: timelineItem.additionalWhitespaces())
FormattedBodyText(text: timelineItem.content.body,
trailingReservedSize: timelineItem.trailingReservedSize)
}
} icon: {
CompoundIcon(\.info, size: .small, relativeTo: .compound.bodyLG)
@@ -28,11 +28,11 @@ struct TextRoomTimelineView: View, TextBasedRoomTimelineViewProtocol {
VStack(alignment: .leading, spacing: 8) {
if let attributedString = timelineItem.content.formattedBody {
FormattedBodyText(attributedString: attributedString,
additionalWhitespacesCount: timelineItem.additionalWhitespaces(),
trailingReservedSize: timelineItem.trailingReservedSize,
boostFontSize: timelineItem.shouldBoost)
} else {
FormattedBodyText(text: timelineItem.body,
additionalWhitespacesCount: timelineItem.additionalWhitespaces(),
trailingReservedSize: timelineItem.trailingReservedSize,
boostFontSize: timelineItem.shouldBoost)
}
@@ -33,11 +33,11 @@ struct VideoRoomTimelineView: View {
if let attributedCaption = timelineItem.content.formattedCaption {
FormattedBodyText(attributedString: attributedCaption,
additionalWhitespacesCount: timelineItem.additionalWhitespaces(),
trailingReservedSize: timelineItem.trailingReservedSize,
boostFontSize: timelineItem.shouldBoost)
} else if let caption = timelineItem.content.caption {
FormattedBodyText(text: caption,
additionalWhitespacesCount: timelineItem.additionalWhitespaces(),
trailingReservedSize: timelineItem.trailingReservedSize,
boostFontSize: timelineItem.shouldBoost)
}
}