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
@@ -21,7 +21,7 @@ enum RoomTimelineItemFixtures {
static var `default`: [RoomTimelineItemProtocol] = [
SeparatorRoomTimelineItem(text: "Yesterday"),
TextRoomTimelineItem(id: UUID().uuidString,
text: "That looks so good!",
body: "That looks so good!",
timestamp: "10:10 AM",
groupState: .single,
isOutgoing: false,
@@ -29,7 +29,7 @@ enum RoomTimelineItemFixtures {
sender: .init(id: "", displayName: "Jacob"),
properties: RoomTimelineItemProperties(isEdited: true)),
TextRoomTimelineItem(id: UUID().uuidString,
text: "Let’s get lunch soon! New salad place opened up 🥗. When are y’all free? 🤗",
body: "Let’s get lunch soon! New salad place opened up 🥗. When are y’all free? 🤗",
timestamp: "10:11 AM",
groupState: .beginning,
isOutgoing: false,
@@ -39,7 +39,7 @@ enum RoomTimelineItemFixtures {
AggregatedReaction(key: "🙌", count: 1, isHighlighted: true)
])),
TextRoomTimelineItem(id: UUID().uuidString,
text: "I can be around on Wednesday. How about some 🌮 instead? Like https://www.tortilla.co.uk/",
body: "I can be around on Wednesday. How about some 🌮 instead? Like https://www.tortilla.co.uk/",
timestamp: "10:11 AM",
groupState: .end,
isOutgoing: false,
@@ -51,21 +51,21 @@ enum RoomTimelineItemFixtures {
])),
SeparatorRoomTimelineItem(text: "Today"),
TextRoomTimelineItem(id: UUID().uuidString,
text: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!",
body: "Wow, cool. Ok, lets go the usual place tomorrow?! Is that too soon? Here’s the menu, let me know what you want it’s on me!",
timestamp: "5 PM",
groupState: .single,
isOutgoing: false,
isEditable: false,
sender: .init(id: "", displayName: "Helena")),
TextRoomTimelineItem(id: UUID().uuidString,
text: "And John's speech was amazing!",
body: "And John's speech was amazing!",
timestamp: "5 PM",
groupState: .beginning,
isOutgoing: true,
isEditable: true,
sender: .init(id: "", displayName: "Bob")),
TextRoomTimelineItem(id: UUID().uuidString,
text: "New home office set up!",
body: "New home office set up!",
timestamp: "5 PM",
groupState: .end,
isOutgoing: true,
@@ -76,12 +76,8 @@ enum RoomTimelineItemFixtures {
AggregatedReaction(key: "😁", count: 3, isHighlighted: false)
])),
TextRoomTimelineItem(id: UUID().uuidString,
text: "",
attributedComponents: [
AttributedStringBuilderComponent(attributedString: "Hol' up", isBlockquote: false, isReply: false),
AttributedStringBuilderComponent(attributedString: "New home office set up!", isBlockquote: true, isReply: false),
AttributedStringBuilderComponent(attributedString: "That's amazing! Congrats 🥳", isBlockquote: false, isReply: false)
],
body: "",
formattedBody: AttributedStringBuilder().fromHTML("Hol' up <blockquote>New home office set up!</blockquote>That's amazing! Congrats 🥳"),
timestamp: "5 PM",
groupState: .single,
isOutgoing: false,
@@ -202,7 +198,7 @@ enum RoomTimelineItemFixtures {
private extension TextRoomTimelineItem {
init(text: String, groupState: TimelineItemGroupState = .single, senderDisplayName: String) {
self.init(id: UUID().uuidString,
text: text,
body: text,
timestamp: "10:47 am",
groupState: groupState,
isOutgoing: senderDisplayName == "Alice",
@@ -115,7 +115,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return .none
}
if let fileURL = item.cachedFileURL {
return .displayFile(fileURL: fileURL, title: item.text)
return .displayFile(fileURL: fileURL, title: item.body)
}
return .none
case let item as VideoRoomTimelineItem:
@@ -125,7 +125,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return .none
}
if let videoURL = item.cachedVideoURL {
return .displayVideo(videoURL: videoURL, title: item.text)
return .displayVideo(videoURL: videoURL, title: item.body)
}
return .none
case let item as FileRoomTimelineItem:
@@ -135,7 +135,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return .none
}
if let fileURL = item.cachedFileURL {
return .displayFile(fileURL: fileURL, title: item.text)
return .displayFile(fileURL: fileURL, title: item.body)
}
return .none
default:
@@ -337,7 +337,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return
}
let fileExtension = movieFileExtension(for: timelineItem.text)
let fileExtension = movieFileExtension(for: timelineItem.body)
switch await mediaProvider.loadFileFromSource(source, fileExtension: fileExtension) {
case .success(let fileURL):
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
@@ -383,7 +383,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
// This is not great. We could better estimate file extension from the mimetype.
guard let fileExtension = timelineItem.text.split(separator: ".").last else {
guard let fileExtension = timelineItem.body.split(separator: ".").last else {
return
}
switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) {
@@ -411,7 +411,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
// This is not great. We could better estimate file extension from the mimetype.
guard let fileExtension = timelineItem.text.split(separator: ".").last else {
guard let fileExtension = timelineItem.body.split(separator: ".").last else {
return
}
switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) {
@@ -34,7 +34,7 @@ enum TimelineItemGroupState: Hashable {
}
protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol {
var text: String { get }
var body: String { get }
var timestamp: String { get }
var shouldShowSenderDetails: Bool { get }
var groupState: TimelineItemGroupState { get }
@@ -18,8 +18,8 @@ import UIKit
struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
let text: String
var attributedComponents: [AttributedStringBuilderComponent]?
let body: String
var formattedBody: AttributedString?
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
@@ -18,7 +18,7 @@ import UIKit
struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
let text: String
let body: String
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
@@ -19,7 +19,7 @@ import UniformTypeIdentifiers
struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
let text: String
let body: String
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
@@ -18,8 +18,8 @@ import UIKit
struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
let text: String
var attributedComponents: [AttributedStringBuilderComponent]?
let body: String
var formattedBody: AttributedString?
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
@@ -18,8 +18,8 @@ import UIKit
struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
let text: String
var attributedComponents: [AttributedStringBuilderComponent]?
let body: String
var formattedBody: AttributedString?
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
@@ -18,7 +18,7 @@ import UIKit
struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
let text: String
let body: String
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
@@ -24,7 +24,7 @@ struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable,
}
let id: String
let text: String
let body: String
let encryptionType: EncryptionType
let timestamp: String
let groupState: TimelineItemGroupState
@@ -19,7 +19,7 @@ import UIKit
struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
let text: String
let body: String
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
@@ -18,7 +18,7 @@ import UIKit
struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
let text: String
let body: String
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
@@ -18,7 +18,7 @@ import UIKit
struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
let text: String
let body: String
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
@@ -18,7 +18,7 @@ import UIKit
struct UnsupportedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
let text: String
let body: String
let eventType: String
let error: String
@@ -98,7 +98,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
UnsupportedRoomTimelineItem(id: eventItemProxy.id,
text: ElementL10n.roomTimelineItemUnsupported,
body: ElementL10n.roomTimelineItemUnsupported,
eventType: eventType,
error: error,
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
@@ -124,7 +124,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
}
return StickerRoomTimelineItem(id: eventItemProxy.id,
text: body,
body: body,
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
@@ -154,7 +154,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
}
return EncryptedRoomTimelineItem(id: eventItemProxy.id,
text: ElementL10n.roomTimelineUnableToDecrypt,
body: ElementL10n.roomTimelineUnableToDecrypt,
encryptionType: encryptionType,
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
@@ -168,7 +168,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
RedactedRoomTimelineItem(id: eventItemProxy.id,
text: ElementL10n.eventRedacted,
body: ElementL10n.eventRedacted,
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
@@ -180,12 +180,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildFallbackTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
let attributedText = attributedStringBuilder.fromPlain(eventItemProxy.body)
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
let formattedBody = attributedStringBuilder.fromPlain(eventItemProxy.body)
return TextRoomTimelineItem(id: eventItemProxy.id,
text: eventItemProxy.body ?? "",
attributedComponents: attributedComponents,
body: eventItemProxy.body ?? "",
formattedBody: formattedBody,
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
@@ -199,12 +198,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
_ message: MessageTimelineItem<TextMessageContent>,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
let formattedBody = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
return TextRoomTimelineItem(id: message.id,
text: message.body,
attributedComponents: attributedComponents,
body: message.body,
formattedBody: formattedBody,
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
@@ -226,7 +224,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
}
return ImageRoomTimelineItem(id: message.id,
text: message.body,
body: message.body,
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
@@ -254,7 +252,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
}
return VideoRoomTimelineItem(id: message.id,
text: message.body,
body: message.body,
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
@@ -277,7 +275,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
FileRoomTimelineItem(id: message.id,
text: message.body,
body: message.body,
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
@@ -294,12 +292,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
_ message: MessageTimelineItem<NoticeMessageContent>,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
let formattedBody = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
return NoticeRoomTimelineItem(id: message.id,
text: message.body,
attributedComponents: attributedComponents,
body: message.body,
formattedBody: formattedBody,
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
@@ -316,18 +313,16 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
let name = eventItemProxy.sender.displayName ?? eventItemProxy.sender.id
var attributedText: AttributedString?
var formattedBody: AttributedString?
if let htmlBody = message.htmlBody {
attributedText = attributedStringBuilder.fromHTML("* \(name) \(htmlBody)")
formattedBody = attributedStringBuilder.fromHTML("* \(name) \(htmlBody)")
} else {
attributedText = attributedStringBuilder.fromPlain("* \(name) \(message.body)")
formattedBody = attributedStringBuilder.fromPlain("* \(name) \(message.body)")
}
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
return EmoteRoomTimelineItem(id: message.id,
text: message.body,
attributedComponents: attributedComponents,
body: message.body,
formattedBody: formattedBody,
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
@@ -381,7 +376,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildStateTimelineItem(eventItemProxy: EventTimelineItemProxy, text: String, isOutgoing: Bool) -> RoomTimelineItemProtocol {
StateRoomTimelineItem(id: eventItemProxy.id,
text: text,
body: text,
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
groupState: .single,
isOutgoing: isOutgoing,