Add permalink detection and custom attributed string attributes (#536)

* Pass full attributed string to TimelineItems and let the UI level handle blockquote coalescing
* Add permalink detection and attribute embedding within the attributed string
This commit is contained in:
Stefan Ceriu
2023-02-06 16:59:32 +02:00
committed by GitHub
parent 2734a18217
commit f8e6179933
38 changed files with 287 additions and 171 deletions
@@ -245,14 +245,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
case .react:
callback?(.displayEmojiPicker(itemId: item.id))
case .copy:
UIPasteboard.general.string = item.text
UIPasteboard.general.string = item.body
case .edit:
state.bindings.composerFocused = true
state.bindings.composerText = item.text
state.bindings.composerText = item.body
state.composerMode = .edit(originalItemId: item.id)
case .quote:
state.bindings.composerFocused = true
state.bindings.composerText = "> \(item.text)"
state.bindings.composerText = "> \(item.body)"
case .copyPermalink:
do {
let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: item.id, roomIdentifier: timelineController.roomID)
@@ -22,10 +22,10 @@ struct EmoteRoomTimelineView: View {
var body: some View {
TimelineStyler(timelineItem: timelineItem) {
if let attributedComponents = timelineItem.attributedComponents {
FormattedBodyText(attributedComponents: attributedComponents)
if let attributedString = timelineItem.formattedBody {
FormattedBodyText(attributedString: attributedString)
} else {
FormattedBodyText(text: timelineItem.text)
FormattedBodyText(text: timelineItem.body)
}
}
}
@@ -53,7 +53,7 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
private static func itemWith(text: String, timestamp: String, senderId: String) -> EmoteRoomTimelineItem {
EmoteRoomTimelineItem(id: UUID().uuidString,
text: text,
body: text,
timestamp: timestamp,
groupState: .single,
isOutgoing: false,
@@ -21,7 +21,7 @@ struct EncryptedRoomTimelineView: View {
var body: some View {
TimelineStyler(timelineItem: timelineItem) {
Label(timelineItem.text, systemImage: "lock.shield")
Label(timelineItem.body, systemImage: "lock.shield")
.labelStyle(RoomTimelineViewLabelStyle())
}
}
@@ -66,7 +66,7 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider {
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> EncryptedRoomTimelineItem {
EncryptedRoomTimelineItem(id: UUID().uuidString,
text: text,
body: text,
encryptionType: .unknown,
timestamp: timestamp,
groupState: .single,
@@ -25,7 +25,7 @@ struct FileRoomTimelineView: View {
HStack {
Image(systemName: "doc.text.fill")
.foregroundColor(.element.primaryContent)
FormattedBodyText(text: timelineItem.text)
FormattedBodyText(text: timelineItem.body)
}
.padding(.vertical, 12)
.padding(.horizontal, 6)
@@ -44,7 +44,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
static var body: some View {
VStack(spacing: 20.0) {
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
text: "document.pdf",
body: "document.pdf",
timestamp: "Now",
groupState: .single,
isOutgoing: false,
@@ -54,7 +54,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
thumbnailSource: nil))
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
text: "document.docx",
body: "document.docx",
timestamp: "Now",
groupState: .single,
isOutgoing: false,
@@ -64,7 +64,7 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
thumbnailSource: nil))
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
text: "document.txt",
body: "document.txt",
timestamp: "Now",
groupState: .single,
isOutgoing: false,
@@ -76,7 +76,11 @@ struct FormattedBodyTextBubbleLayout: Layout {
struct FormattedBodyText: View {
@Environment(\.timelineStyle) private var timelineStyle
let attributedComponents: [AttributedStringBuilderComponent]
private let attributedComponents: [AttributedStringBuilderComponent]
init(attributedString: AttributedString) {
attributedComponents = attributedString.formattedComponents
}
var body: some View {
if timelineStyle == .bubbles {
@@ -155,7 +159,7 @@ struct FormattedBodyText: View {
extension FormattedBodyText {
init(text: String) {
attributedComponents = [.init(attributedString: AttributedString(text), isBlockquote: false, isReply: false)]
self.init(attributedString: AttributedString(text))
}
}
@@ -204,10 +208,8 @@ struct FormattedBodyText_Previews: PreviewProvider {
VStack(alignment: .leading, spacing: 24.0) {
ForEach(htmlStrings, id: \.self) { htmlString in
let attributedString = attributedStringBuilder.fromHTML(htmlString)
if let components = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedString) {
FormattedBodyText(attributedComponents: components)
if let attributedString = attributedStringBuilder.fromHTML(htmlString) {
FormattedBodyText(attributedString: attributedString)
.previewBubble()
}
}
@@ -72,7 +72,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
static var body: some View {
VStack(spacing: 20.0) {
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
text: "Some image",
body: "Some image",
timestamp: "Now",
groupState: .single,
isOutgoing: false,
@@ -81,7 +81,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
source: nil))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
text: "Some other image",
body: "Some other image",
timestamp: "Now",
groupState: .single,
isOutgoing: false,
@@ -90,7 +90,7 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
source: nil))
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
text: "Blurhashed image",
body: "Blurhashed image",
timestamp: "Now",
groupState: .single,
isOutgoing: false,
@@ -26,14 +26,15 @@ struct NoticeRoomTimelineView: View {
// adds additional padding so the spacing between the icon and text is inconsistent.
// Spacing: 6 = label spacing - formatted text padding
HStack(alignment: .firstTextBaseline, spacing: 6) {
HStack(alignment: .firstTextBaseline, spacing: 6.0) {
Image(systemName: "info.bubble").padding(.top, 2.0)
.foregroundColor(.element.secondaryContent)
if let attributedComponents = timelineItem.attributedComponents {
FormattedBodyText(attributedComponents: attributedComponents)
if let attributedString = timelineItem.formattedBody {
FormattedBodyText(attributedString: attributedString)
} else {
FormattedBodyText(text: timelineItem.text)
FormattedBodyText(text: timelineItem.body)
}
}
.padding(.leading, 4) // Trailing padding is provided by FormattedBodyText
@@ -63,7 +64,7 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
private static func itemWith(text: String, timestamp: String, senderId: String) -> NoticeRoomTimelineItem {
NoticeRoomTimelineItem(id: UUID().uuidString,
text: text,
body: text,
timestamp: timestamp,
groupState: .single,
isOutgoing: false,
@@ -43,7 +43,7 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider {
VStack(alignment: .leading, spacing: 0) {
RoomTimelineViewProvider.separator(.init(text: "Today"))
RoomTimelineViewProvider.text(.init(id: "",
text: "This is another message",
body: "This is another message",
timestamp: "",
groupState: .single,
isOutgoing: true,
@@ -54,7 +54,7 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider {
RoomTimelineViewProvider.separator(.init(text: "Today"))
RoomTimelineViewProvider.text(.init(id: "",
text: "This is a message",
body: "This is a message",
timestamp: "",
groupState: .single,
isOutgoing: false,
@@ -22,7 +22,7 @@ struct RedactedRoomTimelineView: View {
var body: some View {
TimelineStyler(timelineItem: timelineItem) {
Label(timelineItem.text, systemImage: "trash")
Label(timelineItem.body, systemImage: "trash")
.labelStyle(RoomTimelineViewLabelStyle())
.imageScale(.small) // Smaller icon so that the bubble remains rounded on the outside.
}
@@ -43,7 +43,7 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider {
private static func itemWith(text: String, timestamp: String, senderId: String) -> RedactedRoomTimelineItem {
RedactedRoomTimelineItem(id: UUID().uuidString,
text: text,
body: text,
timestamp: timestamp,
groupState: .single,
isOutgoing: false,
@@ -20,7 +20,7 @@ struct StateRoomTimelineView: View {
let timelineItem: StateRoomTimelineItem
var body: some View {
Text(timelineItem.text)
Text(timelineItem.body)
.font(.element.footnote)
.multilineTextAlignment(.center)
.foregroundColor(.element.secondaryContent)
@@ -41,7 +41,7 @@ struct StateRoomTimelineView_Previews: PreviewProvider {
}
static let item = StateRoomTimelineItem(id: UUID().uuidString,
text: "Alice joined",
body: "Alice joined",
timestamp: "Now",
groupState: .beginning,
isOutgoing: false,
@@ -31,7 +31,7 @@ struct StickerRoomTimelineView: View {
.frame(maxHeight: 300)
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
}
.accessibilityLabel(timelineItem.text)
.accessibilityLabel(timelineItem.body)
}
private var placeholder: some View {
@@ -57,7 +57,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
static var body: some View {
VStack(spacing: 20.0) {
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
text: "Some image",
body: "Some image",
timestamp: "Now",
groupState: .single,
isOutgoing: false,
@@ -66,7 +66,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
imageURL: URL.picturesDirectory))
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
text: "Some other image",
body: "Some other image",
timestamp: "Now",
groupState: .single,
isOutgoing: false,
@@ -75,7 +75,7 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
imageURL: URL.picturesDirectory))
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
text: "Blurhashed image",
body: "Blurhashed image",
timestamp: "Now",
groupState: .single,
isOutgoing: false,
@@ -22,10 +22,10 @@ struct TextRoomTimelineView: View {
var body: some View {
TimelineStyler(timelineItem: timelineItem) {
if let attributedComponents = timelineItem.attributedComponents {
FormattedBodyText(attributedComponents: attributedComponents)
if let attributedString = timelineItem.formattedBody {
FormattedBodyText(attributedString: attributedString)
} else {
FormattedBodyText(text: timelineItem.text)
FormattedBodyText(text: timelineItem.body)
}
}
}
@@ -65,7 +65,7 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> TextRoomTimelineItem {
TextRoomTimelineItem(id: UUID().uuidString,
text: text,
body: text,
timestamp: timestamp,
groupState: .single,
isOutgoing: isOutgoing,
@@ -23,7 +23,7 @@ struct UnsupportedRoomTimelineView: View {
TimelineStyler(timelineItem: timelineItem) {
Label {
VStack(alignment: .leading) {
Text("\(timelineItem.text): \(timelineItem.eventType)")
Text("\(timelineItem.body): \(timelineItem.eventType)")
.fixedSize(horizontal: false, vertical: true)
Text(timelineItem.error)
@@ -62,7 +62,7 @@ struct UnsupportedRoomTimelineView_Previews: PreviewProvider {
private static func itemWith(text: String, timestamp: String, isOutgoing: Bool, senderId: String) -> UnsupportedRoomTimelineItem {
UnsupportedRoomTimelineItem(id: UUID().uuidString,
text: text,
body: text,
eventType: "Some Event Type",
error: "Something went wrong",
timestamp: timestamp,
@@ -67,7 +67,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
static var body: some View {
VStack(spacing: 20.0) {
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
text: "Some video",
body: "Some video",
timestamp: "Now",
groupState: .single,
isOutgoing: false,
@@ -78,7 +78,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
thumbnailSource: nil))
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
text: "Some other video",
body: "Some other video",
timestamp: "Now",
groupState: .single,
isOutgoing: false,
@@ -89,7 +89,7 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
thumbnailSource: nil))
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
text: "Blurhashed video",
body: "Blurhashed video",
timestamp: "Now",
groupState: .single,
isOutgoing: false,