Fix vector-im/element-x-ios/issues/134 - Prevent home screen room last message attributed string from being computed on every update

Add async/await methods to the AttributedStringBuilder and adopt them throughout the app
This commit is contained in:
Stefan Ceriu
2022-07-22 10:25:17 +03:00
committed by GitHub
parent 2f4fb81aa5
commit e1eee9465e
11 changed files with 147 additions and 89 deletions
@@ -32,6 +32,12 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
} }
} }
func fromPlain(_ string: String?) async -> AttributedString? {
await Task.detached {
fromPlain(string)
}.value
}
func fromPlain(_ string: String?) -> AttributedString? { func fromPlain(_ string: String?) -> AttributedString? {
guard let string = string else { guard let string = string else {
return nil return nil
@@ -44,6 +50,12 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
return try? AttributedString(mutableAttributedString, including: \.elementX) return try? AttributedString(mutableAttributedString, including: \.elementX)
} }
func fromHTML(_ htmlString: String?) async -> AttributedString? {
await Task.detached {
fromHTML(htmlString)
}.value
}
// Do not use the default HTML renderer of NSAttributedString because this method // Do not use the default HTML renderer of NSAttributedString because this method
// runs on the UI thread which we want to avoid because renderHTMLString is called // runs on the UI thread which we want to avoid because renderHTMLString is called
// most of the time from a background thread. // most of the time from a background thread.
@@ -15,7 +15,10 @@ struct AttributedStringBuilderComponent: Hashable {
protocol AttributedStringBuilderProtocol { protocol AttributedStringBuilderProtocol {
func fromPlain(_ string: String?) -> AttributedString? func fromPlain(_ string: String?) -> AttributedString?
func fromPlain(_ string: String?) async -> AttributedString?
func fromHTML(_ htmlString: String?) -> AttributedString? func fromHTML(_ htmlString: String?) -> AttributedString?
func fromHTML(_ htmlString: String?) async -> AttributedString?
func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]? func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]?
} }
@@ -70,7 +70,7 @@ struct HomeScreenRoom: Identifiable, Equatable {
var displayName: String? var displayName: String?
let topic: String? var topic: String?
var lastMessage: String? var lastMessage: String?
var avatar: UIImage? var avatar: UIImage?
@@ -23,6 +23,11 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
private let attributedStringBuilder: AttributedStringBuilderProtocol private let attributedStringBuilder: AttributedStringBuilderProtocol
private var roomUpdateListeners = Set<AnyCancellable>() private var roomUpdateListeners = Set<AnyCancellable>()
private var roomsUpdateTask: Task<Void, Never>? {
willSet {
roomsUpdateTask?.cancel()
}
}
private var roomSummaries: [RoomSummaryProtocol]? { private var roomSummaries: [RoomSummaryProtocol]? {
didSet { didSet {
@@ -56,14 +61,29 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
} }
func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol]) { func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol]) {
self.roomSummaries = roomSummaries roomsUpdateTask = Task {
await updateWithRoomSummaries(roomSummaries)
state.rooms = roomSummaries.map { roomSummary in }
buildOrUpdateRoomFromSummary(roomSummary) }
private func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol]) async {
var rooms = [HomeScreenRoom]()
for summary in roomSummaries {
if Task.isCancelled {
return
}
rooms.append(await buildOrUpdateRoomForSummary(summary))
} }
roomUpdateListeners.removeAll() if Task.isCancelled {
return
}
state.rooms = rooms
self.roomSummaries = roomSummaries
roomUpdateListeners.removeAll()
roomSummaries.forEach { roomSummary in roomSummaries.forEach { roomSummary in
roomSummary.callbacks roomSummary.callbacks
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
@@ -72,10 +92,16 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
return return
} }
switch callback { Task {
case .updatedData:
if let index = self.state.rooms.firstIndex(where: { $0.id == roomSummary.id }) { if let index = self.state.rooms.firstIndex(where: { $0.id == roomSummary.id }) {
self.state.rooms[index] = self.buildOrUpdateRoomFromSummary(roomSummary) switch callback {
case .updatedLastMessage:
var room = self.state.rooms[index]
room.lastMessage = await self.lastMessageFromEventBrief(roomSummary.lastMessage)
self.state.rooms[index] = room
default:
self.state.rooms[index] = await self.buildOrUpdateRoomForSummary(roomSummary)
}
} }
} }
} }
@@ -110,14 +136,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
Task { await roomSummary.loadDetails() } Task { await roomSummary.loadDetails() }
} }
private func buildOrUpdateRoomFromSummary(_ roomSummary: RoomSummaryProtocol) -> HomeScreenRoom { private func buildOrUpdateRoomForSummary(_ roomSummary: RoomSummaryProtocol) async -> HomeScreenRoom {
let lastMessage = lastMessageFromEventBrief(roomSummary.lastMessage)
guard var room = state.rooms.first(where: { $0.id == roomSummary.id }) else { guard var room = state.rooms.first(where: { $0.id == roomSummary.id }) else {
return HomeScreenRoom(id: roomSummary.id, return HomeScreenRoom(id: roomSummary.id,
displayName: roomSummary.displayName, displayName: roomSummary.displayName,
topic: roomSummary.topic, topic: roomSummary.topic,
lastMessage: lastMessage, lastMessage: await lastMessageFromEventBrief(roomSummary.lastMessage),
avatar: roomSummary.avatar, avatar: roomSummary.avatar,
isDirect: roomSummary.isDirect, isDirect: roomSummary.isDirect,
isEncrypted: roomSummary.isEncrypted, isEncrypted: roomSummary.isEncrypted,
@@ -127,12 +151,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
room.avatar = roomSummary.avatar room.avatar = roomSummary.avatar
room.displayName = roomSummary.displayName room.displayName = roomSummary.displayName
room.lastMessage = lastMessage room.topic = roomSummary.topic
return room return room
} }
private func lastMessageFromEventBrief(_ eventBrief: EventBrief?) -> String? { private func lastMessageFromEventBrief(_ eventBrief: EventBrief?) async -> String? {
guard let eventBrief = eventBrief else { guard let eventBrief = eventBrief else {
return nil return nil
} }
@@ -140,7 +164,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
let senderDisplayName = senderDisplayNameForBrief(eventBrief) let senderDisplayName = senderDisplayNameForBrief(eventBrief)
if let htmlBody = eventBrief.htmlBody, if let htmlBody = eventBrief.htmlBody,
let lastMessageAttributedString = attributedStringBuilder.fromHTML(htmlBody) { let lastMessageAttributedString = await attributedStringBuilder.fromHTML(htmlBody) {
return "\(senderDisplayName): \(String(lastMessageAttributedString.characters))" return "\(senderDisplayName): \(String(lastMessageAttributedString.characters))"
} else { } else {
return "\(senderDisplayName): \(eventBrief.body)" return "\(senderDisplayName): \(eventBrief.body)"
@@ -57,9 +57,7 @@ class RoomProxy: RoomProxyProtocol {
room.setDelegate(delegate: nil) room.setDelegate(delegate: nil)
} }
var id: String { lazy var id: String = room.id()
room.id()
}
var name: String? { var name: String? {
room.name() room.name()
@@ -46,21 +46,21 @@ class RoomSummary: RoomSummaryProtocol {
roomProxy.isTombstoned roomProxy.isTombstoned
} }
private(set) var avatar: UIImage? {
didSet {
callbacks.send(.updatedAvatar)
}
}
private(set) var displayName: String? { private(set) var displayName: String? {
didSet { didSet {
callbacks.send(.updatedData) callbacks.send(.updatedDisplayName)
} }
} }
private(set) var lastMessage: EventBrief? { private(set) var lastMessage: EventBrief? {
didSet { didSet {
callbacks.send(.updatedData) callbacks.send(.updatedLastMessage)
}
}
private(set) var avatar: UIImage? {
didSet {
callbacks.send(.updatedData)
} }
} }
@@ -10,7 +10,9 @@ import Combine
import UIKit import UIKit
enum RoomSummaryCallback { enum RoomSummaryCallback {
case updatedData case updatedAvatar
case updatedDisplayName
case updatedLastMessage
} }
@MainActor @MainActor
@@ -18,6 +18,11 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
private let memberDetailProvider: MemberDetailProviderProtocol private let memberDetailProvider: MemberDetailProviderProtocol
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var timelineItemsUpdateTask: Task<Void, Never>? {
willSet {
timelineItemsUpdateTask?.cancel()
}
}
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>() let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
@@ -96,10 +101,20 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
} }
private func updateTimelineItems() { private func updateTimelineItems() {
timelineItemsUpdateTask = Task {
await asyncUpdateTimelineItems()
}
}
private func asyncUpdateTimelineItems() async {
var newTimelineItems = [RoomTimelineItemProtocol]() var newTimelineItems = [RoomTimelineItemProtocol]()
var previousMessage: RoomMessageProtocol? var previousMessage: RoomMessageProtocol?
for message in timelineProvider.messages { for message in timelineProvider.messages {
if Task.isCancelled {
return
}
let areMessagesFromTheSameDay = haveSameDay(lhs: previousMessage, rhs: message) let areMessagesFromTheSameDay = haveSameDay(lhs: previousMessage, rhs: message)
let shouldAddSectionHeader = !areMessagesFromTheSameDay let shouldAddSectionHeader = !areMessagesFromTheSameDay
@@ -111,13 +126,17 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
let areMessagesFromTheSameSender = (previousMessage?.sender == message.sender) let areMessagesFromTheSameSender = (previousMessage?.sender == message.sender)
let shouldShowSenderDetails = !areMessagesFromTheSameSender || !areMessagesFromTheSameDay let shouldShowSenderDetails = !areMessagesFromTheSameSender || !areMessagesFromTheSameDay
newTimelineItems.append(timelineItemFactory.buildTimelineItemFor(message: message, newTimelineItems.append(await timelineItemFactory.buildTimelineItemFor(message: message,
isOutgoing: message.sender == userId, isOutgoing: message.sender == userId,
showSenderDetails: shouldShowSenderDetails)) showSenderDetails: shouldShowSenderDetails))
previousMessage = message previousMessage = message
} }
if Task.isCancelled {
return
}
timelineItems = newTimelineItems timelineItems = newTimelineItems
callbacks.send(.updatedTimelineItems) callbacks.send(.updatedTimelineItems)
@@ -22,20 +22,20 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
self.attributedStringBuilder = attributedStringBuilder self.attributedStringBuilder = attributedStringBuilder
} }
func buildTimelineItemFor(message: RoomMessageProtocol, isOutgoing: Bool, showSenderDetails: Bool) -> RoomTimelineItemProtocol { func buildTimelineItemFor(message: RoomMessageProtocol, isOutgoing: Bool, showSenderDetails: Bool) async -> RoomTimelineItemProtocol {
let displayName = memberDetailProvider.displayNameForUserId(message.sender) let displayName = memberDetailProvider.displayNameForUserId(message.sender)
let avatarURL = memberDetailProvider.avatarURLStringForUserId(message.sender) let avatarURL = memberDetailProvider.avatarURLStringForUserId(message.sender)
let avatarImage = mediaProvider.imageFromURLString(avatarURL) let avatarImage = mediaProvider.imageFromURLString(avatarURL)
switch message { switch message {
case let message as TextRoomMessage: case let message as TextRoomMessage:
return buildTextTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage) return await buildTextTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
case let message as ImageRoomMessage: case let message as ImageRoomMessage:
return buildImageTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage) return await buildImageTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
case let message as NoticeRoomMessage: case let message as NoticeRoomMessage:
return buildNoticeTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage) return await buildNoticeTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
case let message as EmoteRoomMessage: case let message as EmoteRoomMessage:
return buildEmoteTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage) return await buildEmoteTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
default: default:
fatalError("Unknown room message.") fatalError("Unknown room message.")
} }
@@ -47,8 +47,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
_ isOutgoing: Bool, _ isOutgoing: Bool,
_ showSenderDetails: Bool, _ showSenderDetails: Bool,
_ displayName: String?, _ displayName: String?,
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol { _ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body)) let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText) let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
return TextRoomTimelineItem(id: message.id, return TextRoomTimelineItem(id: message.id,
@@ -66,7 +66,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
_ isOutgoing: Bool, _ isOutgoing: Bool,
_ showSenderDetails: Bool, _ showSenderDetails: Bool,
_ displayName: String?, _ displayName: String?,
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol { _ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
var aspectRatio: CGFloat? var aspectRatio: CGFloat?
if let width = message.width, if let width = message.width,
let height = message.height { let height = message.height {
@@ -93,8 +93,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
_ isOutgoing: Bool, _ isOutgoing: Bool,
_ showSenderDetails: Bool, _ showSenderDetails: Bool,
_ displayName: String?, _ displayName: String?,
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol { _ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body)) let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText) let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
return NoticeRoomTimelineItem(id: message.id, return NoticeRoomTimelineItem(id: message.id,
@@ -112,8 +112,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
_ isOutgoing: Bool, _ isOutgoing: Bool,
_ showSenderDetails: Bool, _ showSenderDetails: Bool,
_ displayName: String?, _ displayName: String?,
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol { _ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body)) let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText) let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
return EmoteRoomTimelineItem(id: message.id, return EmoteRoomTimelineItem(id: message.id,
@@ -10,5 +10,5 @@ import Foundation
@MainActor @MainActor
protocol RoomTimelineItemFactoryProtocol { protocol RoomTimelineItemFactoryProtocol {
func buildTimelineItemFor(message: RoomMessageProtocol, isOutgoing: Bool, showSenderDetails: Bool) -> RoomTimelineItemProtocol func buildTimelineItemFor(message: RoomMessageProtocol, isOutgoing: Bool, showSenderDetails: Bool) async -> RoomTimelineItemProtocol
} }
@@ -13,14 +13,14 @@ class AttributedStringBuilderTests: XCTestCase {
let attributedStringBuilder = AttributedStringBuilder() let attributedStringBuilder = AttributedStringBuilder()
let maxHeaderPointSize = ceil(UIFont.preferredFont(forTextStyle: .body).pointSize * 1.2) let maxHeaderPointSize = ceil(UIFont.preferredFont(forTextStyle: .body).pointSize * 1.2)
func testRenderHTMLStringWithHeaders() { func testRenderHTMLStringWithHeaders() async {
let h1HTMLString = "<h1>Large Heading</h1>" let h1HTMLString = "<h1>Large Heading</h1>"
let h2HTMLString = "<h2>Smaller Heading</h2>" let h2HTMLString = "<h2>Smaller Heading</h2>"
let h3HTMLString = "<h3>Acceptable Heading</h3>" let h3HTMLString = "<h3>Acceptable Heading</h3>"
guard let h1AttributedString = attributedStringBuilder.fromHTML(h1HTMLString), guard let h1AttributedString = await attributedStringBuilder.fromHTML(h1HTMLString),
let h2AttributedString = attributedStringBuilder.fromHTML(h2HTMLString), let h2AttributedString = await attributedStringBuilder.fromHTML(h2HTMLString),
let h3AttributedString = attributedStringBuilder.fromHTML(h3HTMLString) else { let h3AttributedString = await attributedStringBuilder.fromHTML(h3HTMLString) else {
XCTFail("Could not build the attributed string") XCTFail("Could not build the attributed string")
return return
} }
@@ -48,10 +48,10 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssert(h1Font.pointSize <= maxHeaderPointSize) XCTAssert(h1Font.pointSize <= maxHeaderPointSize)
} }
func testRenderHTMLStringWithPreCode() { func testRenderHTMLStringWithPreCode() async {
let htmlString = "<pre><code>1\n2\n3\n4\n</code></pre>" let htmlString = "<pre><code>1\n2\n3\n4\n</code></pre>"
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { guard let attributedString = await attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string") XCTFail("Could not build the attributed string")
return return
} }
@@ -68,10 +68,10 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertEqual(regex.numberOfMatches(in: string, options: [], range: .init(location: 0, length: string.count)), 3) XCTAssertEqual(regex.numberOfMatches(in: string, options: [], range: .init(location: 0, length: string.count)), 3)
} }
func testRenderHTMLStringWithLink() { func testRenderHTMLStringWithLink() async {
let htmlString = "This text contains a <a href=\"https://www.matrix.org/\">link</a>." let htmlString = "This text contains a <a href=\"https://www.matrix.org/\">link</a>."
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { guard let attributedString = await attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string") XCTFail("Could not build the attributed string")
return return
} }
@@ -85,10 +85,10 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertEqual(link?.host, "www.matrix.org") XCTAssertEqual(link?.host, "www.matrix.org")
} }
func testRenderPlainStringWithLink() { func testRenderPlainStringWithLink() async {
let plainString = "This text contains a https://www.matrix.org link." let plainString = "This text contains a https://www.matrix.org link."
guard let attributedString = attributedStringBuilder.fromPlain(plainString) else { guard let attributedString = await attributedStringBuilder.fromPlain(plainString) else {
XCTFail("Could not build the attributed string") XCTFail("Could not build the attributed string")
return return
} }
@@ -102,14 +102,14 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertEqual(link?.host, "www.matrix.org") XCTAssertEqual(link?.host, "www.matrix.org")
} }
func testRenderHTMLStringWithLinkInHeader() { func testRenderHTMLStringWithLinkInHeader() async {
let h1HTMLString = "<h1><a href=\"https://www.matrix.org/\">Matrix.org</a></h1>" let h1HTMLString = "<h1><a href=\"https://www.matrix.org/\">Matrix.org</a></h1>"
let h2HTMLString = "<h2><a href=\"https://www.matrix.org/\">Matrix.org</a></h2>" let h2HTMLString = "<h2><a href=\"https://www.matrix.org/\">Matrix.org</a></h2>"
let h3HTMLString = "<h3><a href=\"https://www.matrix.org/\">Matrix.org</a></h3>" let h3HTMLString = "<h3><a href=\"https://www.matrix.org/\">Matrix.org</a></h3>"
guard let h1AttributedString = attributedStringBuilder.fromHTML(h1HTMLString), guard let h1AttributedString = await attributedStringBuilder.fromHTML(h1HTMLString),
let h2AttributedString = attributedStringBuilder.fromHTML(h2HTMLString), let h2AttributedString = await attributedStringBuilder.fromHTML(h2HTMLString),
let h3AttributedString = attributedStringBuilder.fromHTML(h3HTMLString) else { let h3AttributedString = await attributedStringBuilder.fromHTML(h3HTMLString) else {
XCTFail("Could not build the attributed string") XCTFail("Could not build the attributed string")
return return
} }
@@ -140,10 +140,10 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertEqual(h3AttributedString.runs.first?.link?.host, "www.matrix.org") XCTAssertEqual(h3AttributedString.runs.first?.link?.host, "www.matrix.org")
} }
func testRenderHTMLStringWithIFrame() { func testRenderHTMLStringWithIFrame() async {
let htmlString = "<iframe src=\"https://www.matrix.org/\"></iframe>" let htmlString = "<iframe src=\"https://www.matrix.org/\"></iframe>"
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { guard let attributedString = await attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string") XCTFail("Could not build the attributed string")
return return
} }
@@ -151,38 +151,38 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertNil(attributedString.uiKit.attachment, "iFrame attachments should be removed as they're not included in the allowedHTMLTags array.") XCTAssertNil(attributedString.uiKit.attachment, "iFrame attachments should be removed as they're not included in the allowedHTMLTags array.")
} }
func testUserIdLink() { func testUserIdLink() async {
let userId = "@user:matrix.org" let userId = "@user:matrix.org"
let string = "The user is \(userId)." let string = "The user is \(userId)."
checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expected: userId) checkMatrixEntityLinkIn(attributedString: await attributedStringBuilder.fromHTML(string), expected: userId)
checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expected: userId) checkMatrixEntityLinkIn(attributedString: await attributedStringBuilder.fromPlain(string), expected: userId)
} }
func testRoomAliasLink() { func testRoomAliasLink() async {
let roomAlias = "#matrix:matrix.org" let roomAlias = "#matrix:matrix.org"
let string = "The room alias is \(roomAlias)." let string = "The room alias is \(roomAlias)."
checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expected: roomAlias) checkMatrixEntityLinkIn(attributedString: await attributedStringBuilder.fromHTML(string), expected: roomAlias)
checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expected: roomAlias) checkMatrixEntityLinkIn(attributedString: await attributedStringBuilder.fromPlain(string), expected: roomAlias)
} }
func testRoomIdLink() { func testRoomIdLink() async {
let roomId = "!roomidentifier:matrix.org" let roomId = "!roomidentifier:matrix.org"
let string = "The room is \(roomId)." let string = "The room is \(roomId)."
checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expected: roomId) checkMatrixEntityLinkIn(attributedString: await attributedStringBuilder.fromHTML(string), expected: roomId)
checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expected: roomId) checkMatrixEntityLinkIn(attributedString: await attributedStringBuilder.fromPlain(string), expected: roomId)
} }
func testEventIdLink() { func testEventIdLink() async {
let eventId = "$eventidentifier:matrix.org" let eventId = "$eventidentifier:matrix.org"
let string = "The event is \(eventId)." let string = "The event is \(eventId)."
checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromHTML(string), expected: eventId) checkMatrixEntityLinkIn(attributedString: await attributedStringBuilder.fromHTML(string), expected: eventId)
checkMatrixEntityLinkIn(attributedString: attributedStringBuilder.fromPlain(string), expected: eventId) checkMatrixEntityLinkIn(attributedString: await attributedStringBuilder.fromPlain(string), expected: eventId)
} }
func testDefaultFont() { func testDefaultFont() async {
let htmlString = "<b>Test</b> <i>string</i>." let htmlString = "<b>Test</b> <i>string</i>."
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { guard let attributedString = await attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string") XCTFail("Could not build the attributed string")
return return
} }
@@ -194,10 +194,10 @@ class AttributedStringBuilderTests: XCTestCase {
} }
} }
func testDefaultForegroundColor() { func testDefaultForegroundColor() async {
let htmlString = "<b>Test</b> <i>string</i>." let htmlString = "<b>Test</b> <i>string</i>."
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { guard let attributedString = await attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string") XCTFail("Could not build the attributed string")
return return
} }
@@ -209,11 +209,11 @@ class AttributedStringBuilderTests: XCTestCase {
} }
} }
func testCustomForegroundColor() { func testCustomForegroundColor() async {
// swiftlint:disable:next line_length // swiftlint:disable:next line_length
let htmlString = "<font color=\"#ff00be\">R</font><font color=\"#ff0082\">a</font><font color=\"#ff0047\">i</font><font color=\"#ff5800\">n </font><font color=\"#ffa300\">w</font><font color=\"#d2ba00\">w</font><font color=\"#97ca00\">w</font><font color=\"#3ed500\">.</font><font color=\"#00dd00\">m</font><font color=\"#00e251\">a</font><font color=\"#00e595\">t</font><font color=\"#00e7d6\">r</font><font color=\"#00e7ff\">i</font><font color=\"#00e6ff\">x</font><font color=\"#00e3ff\">.</font><font color=\"#00dbff\">o</font><font color=\"#00ceff\">r</font><font color=\"#00baff\">g</font><font color=\"#f477ff\"> b</font><font color=\"#ff3aff\">o</font><font color=\"#ff00fb\">w</font>" let htmlString = "<font color=\"#ff00be\">R</font><font color=\"#ff0082\">a</font><font color=\"#ff0047\">i</font><font color=\"#ff5800\">n </font><font color=\"#ffa300\">w</font><font color=\"#d2ba00\">w</font><font color=\"#97ca00\">w</font><font color=\"#3ed500\">.</font><font color=\"#00dd00\">m</font><font color=\"#00e251\">a</font><font color=\"#00e595\">t</font><font color=\"#00e7d6\">r</font><font color=\"#00e7ff\">i</font><font color=\"#00e6ff\">x</font><font color=\"#00e3ff\">.</font><font color=\"#00dbff\">o</font><font color=\"#00ceff\">r</font><font color=\"#00baff\">g</font><font color=\"#f477ff\"> b</font><font color=\"#ff3aff\">o</font><font color=\"#ff00fb\">w</font>"
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { guard let attributedString = await attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string") XCTFail("Could not build the attributed string")
return return
} }
@@ -234,10 +234,10 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertTrue(foundLink) XCTAssertTrue(foundLink)
} }
func testSingleBlockquote() { func testSingleBlockquote() async {
let htmlString = "<blockquote>Blockquote</blockquote>" let htmlString = "<blockquote>Blockquote</blockquote>"
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { guard let attributedString = await attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string") XCTFail("Could not build the attributed string")
return return
} }
@@ -256,14 +256,14 @@ class AttributedStringBuilderTests: XCTestCase {
} }
// swiftlint:disable line_length // swiftlint:disable line_length
func testBlockquoteWithinText() { func testBlockquoteWithinText() async {
let htmlString = """ let htmlString = """
The text before the blockquote The text before the blockquote
<blockquote> For 50 years, WWF has been protecting the future of nature. The world's leading conservation organization, WWF works in 100 countries and is supported by 1.2 million members in the United States and close to 5 million globally.</blockquote> <blockquote> For 50 years, WWF has been protecting the future of nature. The world's leading conservation organization, WWF works in 100 countries and is supported by 1.2 million members in the United States and close to 5 million globally.</blockquote>
The text after the blockquote The text after the blockquote
""" """
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { guard let attributedString = await attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string") XCTFail("Could not build the attributed string")
return return
} }
@@ -283,10 +283,10 @@ class AttributedStringBuilderTests: XCTestCase {
// swiftlint:enable line_length // swiftlint:enable line_length
func testBlockquoteWithLink() { func testBlockquoteWithLink() async {
let htmlString = "<blockquote>Blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote>" let htmlString = "<blockquote>Blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote>"
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { guard let attributedString = await attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string") XCTFail("Could not build the attributed string")
return return
} }
@@ -311,14 +311,14 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertNotNil(foundBlockquoteAndLink, "Couldn't find blockquote or link") XCTAssertNotNil(foundBlockquoteAndLink, "Couldn't find blockquote or link")
} }
func testMultipleGroupedBlockquotes() { func testMultipleGroupedBlockquotes() async {
let htmlString = """ let htmlString = """
<blockquote>First blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote> <blockquote>First blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote>
<blockquote>Second blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote> <blockquote>Second blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote>
<blockquote>Third blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote> <blockquote>Third blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote>
""" """
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { guard let attributedString = await attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string") XCTFail("Could not build the attributed string")
return return
} }
@@ -337,7 +337,7 @@ class AttributedStringBuilderTests: XCTestCase {
XCTAssertEqual(numberOfBlockquotes, 3, "Couldn't find all the blockquotes") XCTAssertEqual(numberOfBlockquotes, 3, "Couldn't find all the blockquotes")
} }
func testMultipleSeparatedBlockquotes() { func testMultipleSeparatedBlockquotes() async {
let htmlString = """ let htmlString = """
First First
<blockquote>blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote> <blockquote>blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote>
@@ -347,7 +347,7 @@ class AttributedStringBuilderTests: XCTestCase {
<blockquote>blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote> <blockquote>blockquote with a <a href=\"https://www.matrix.org/\">link</a> in it</blockquote>
""" """
guard let attributedString = attributedStringBuilder.fromHTML(htmlString) else { guard let attributedString = await attributedStringBuilder.fromHTML(htmlString) else {
XCTFail("Could not build the attributed string") XCTFail("Could not build the attributed string")
return return
} }