mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
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:
@@ -32,6 +32,12 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func fromPlain(_ string: String?) async -> AttributedString? {
|
||||
await Task.detached {
|
||||
fromPlain(string)
|
||||
}.value
|
||||
}
|
||||
|
||||
func fromPlain(_ string: String?) -> AttributedString? {
|
||||
guard let string = string else {
|
||||
return nil
|
||||
@@ -44,6 +50,12 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
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
|
||||
// runs on the UI thread which we want to avoid because renderHTMLString is called
|
||||
// most of the time from a background thread.
|
||||
|
||||
@@ -15,7 +15,10 @@ struct AttributedStringBuilderComponent: Hashable {
|
||||
|
||||
protocol AttributedStringBuilderProtocol {
|
||||
func fromPlain(_ string: String?) -> AttributedString?
|
||||
func fromPlain(_ string: String?) async -> AttributedString?
|
||||
|
||||
func fromHTML(_ htmlString: String?) -> AttributedString?
|
||||
func fromHTML(_ htmlString: String?) async -> AttributedString?
|
||||
|
||||
func blockquoteCoalescedComponentsFrom(_ attributedString: AttributedString?) -> [AttributedStringBuilderComponent]?
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
|
||||
var displayName: String?
|
||||
|
||||
let topic: String?
|
||||
var topic: String?
|
||||
var lastMessage: String?
|
||||
|
||||
var avatar: UIImage?
|
||||
|
||||
@@ -23,6 +23,11 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
private let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||
|
||||
private var roomUpdateListeners = Set<AnyCancellable>()
|
||||
private var roomsUpdateTask: Task<Void, Never>? {
|
||||
willSet {
|
||||
roomsUpdateTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private var roomSummaries: [RoomSummaryProtocol]? {
|
||||
didSet {
|
||||
@@ -56,14 +61,29 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
}
|
||||
|
||||
func updateWithRoomSummaries(_ roomSummaries: [RoomSummaryProtocol]) {
|
||||
self.roomSummaries = roomSummaries
|
||||
|
||||
state.rooms = roomSummaries.map { roomSummary in
|
||||
buildOrUpdateRoomFromSummary(roomSummary)
|
||||
roomsUpdateTask = Task {
|
||||
await updateWithRoomSummaries(roomSummaries)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
roomSummary.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
@@ -72,10 +92,16 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
return
|
||||
}
|
||||
|
||||
switch callback {
|
||||
case .updatedData:
|
||||
Task {
|
||||
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() }
|
||||
}
|
||||
|
||||
private func buildOrUpdateRoomFromSummary(_ roomSummary: RoomSummaryProtocol) -> HomeScreenRoom {
|
||||
let lastMessage = lastMessageFromEventBrief(roomSummary.lastMessage)
|
||||
|
||||
private func buildOrUpdateRoomForSummary(_ roomSummary: RoomSummaryProtocol) async -> HomeScreenRoom {
|
||||
guard var room = state.rooms.first(where: { $0.id == roomSummary.id }) else {
|
||||
return HomeScreenRoom(id: roomSummary.id,
|
||||
displayName: roomSummary.displayName,
|
||||
topic: roomSummary.topic,
|
||||
lastMessage: lastMessage,
|
||||
lastMessage: await lastMessageFromEventBrief(roomSummary.lastMessage),
|
||||
avatar: roomSummary.avatar,
|
||||
isDirect: roomSummary.isDirect,
|
||||
isEncrypted: roomSummary.isEncrypted,
|
||||
@@ -127,12 +151,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
room.avatar = roomSummary.avatar
|
||||
room.displayName = roomSummary.displayName
|
||||
room.lastMessage = lastMessage
|
||||
|
||||
room.topic = roomSummary.topic
|
||||
|
||||
return room
|
||||
}
|
||||
|
||||
private func lastMessageFromEventBrief(_ eventBrief: EventBrief?) -> String? {
|
||||
private func lastMessageFromEventBrief(_ eventBrief: EventBrief?) async -> String? {
|
||||
guard let eventBrief = eventBrief else {
|
||||
return nil
|
||||
}
|
||||
@@ -140,7 +164,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
let senderDisplayName = senderDisplayNameForBrief(eventBrief)
|
||||
|
||||
if let htmlBody = eventBrief.htmlBody,
|
||||
let lastMessageAttributedString = attributedStringBuilder.fromHTML(htmlBody) {
|
||||
let lastMessageAttributedString = await attributedStringBuilder.fromHTML(htmlBody) {
|
||||
return "\(senderDisplayName): \(String(lastMessageAttributedString.characters))"
|
||||
} else {
|
||||
return "\(senderDisplayName): \(eventBrief.body)"
|
||||
|
||||
@@ -57,9 +57,7 @@ class RoomProxy: RoomProxyProtocol {
|
||||
room.setDelegate(delegate: nil)
|
||||
}
|
||||
|
||||
var id: String {
|
||||
room.id()
|
||||
}
|
||||
lazy var id: String = room.id()
|
||||
|
||||
var name: String? {
|
||||
room.name()
|
||||
|
||||
@@ -46,21 +46,21 @@ class RoomSummary: RoomSummaryProtocol {
|
||||
roomProxy.isTombstoned
|
||||
}
|
||||
|
||||
private(set) var avatar: UIImage? {
|
||||
didSet {
|
||||
callbacks.send(.updatedAvatar)
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var displayName: String? {
|
||||
didSet {
|
||||
callbacks.send(.updatedData)
|
||||
callbacks.send(.updatedDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var lastMessage: EventBrief? {
|
||||
didSet {
|
||||
callbacks.send(.updatedData)
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var avatar: UIImage? {
|
||||
didSet {
|
||||
callbacks.send(.updatedData)
|
||||
callbacks.send(.updatedLastMessage)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ import Combine
|
||||
import UIKit
|
||||
|
||||
enum RoomSummaryCallback {
|
||||
case updatedData
|
||||
case updatedAvatar
|
||||
case updatedDisplayName
|
||||
case updatedLastMessage
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@@ -18,6 +18,11 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
private let memberDetailProvider: MemberDetailProviderProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var timelineItemsUpdateTask: Task<Void, Never>? {
|
||||
willSet {
|
||||
timelineItemsUpdateTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
|
||||
|
||||
@@ -96,10 +101,20 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
}
|
||||
|
||||
private func updateTimelineItems() {
|
||||
timelineItemsUpdateTask = Task {
|
||||
await asyncUpdateTimelineItems()
|
||||
}
|
||||
}
|
||||
|
||||
private func asyncUpdateTimelineItems() async {
|
||||
var newTimelineItems = [RoomTimelineItemProtocol]()
|
||||
|
||||
var previousMessage: RoomMessageProtocol?
|
||||
for message in timelineProvider.messages {
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
|
||||
let areMessagesFromTheSameDay = haveSameDay(lhs: previousMessage, rhs: message)
|
||||
let shouldAddSectionHeader = !areMessagesFromTheSameDay
|
||||
|
||||
@@ -111,13 +126,17 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
let areMessagesFromTheSameSender = (previousMessage?.sender == message.sender)
|
||||
let shouldShowSenderDetails = !areMessagesFromTheSameSender || !areMessagesFromTheSameDay
|
||||
|
||||
newTimelineItems.append(timelineItemFactory.buildTimelineItemFor(message: message,
|
||||
isOutgoing: message.sender == userId,
|
||||
showSenderDetails: shouldShowSenderDetails))
|
||||
newTimelineItems.append(await timelineItemFactory.buildTimelineItemFor(message: message,
|
||||
isOutgoing: message.sender == userId,
|
||||
showSenderDetails: shouldShowSenderDetails))
|
||||
|
||||
previousMessage = message
|
||||
}
|
||||
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
|
||||
timelineItems = newTimelineItems
|
||||
|
||||
callbacks.send(.updatedTimelineItems)
|
||||
|
||||
@@ -22,20 +22,20 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
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 avatarURL = memberDetailProvider.avatarURLStringForUserId(message.sender)
|
||||
let avatarImage = mediaProvider.imageFromURLString(avatarURL)
|
||||
|
||||
switch message {
|
||||
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:
|
||||
return buildImageTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
|
||||
return await buildImageTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
|
||||
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:
|
||||
return buildEmoteTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
|
||||
return await buildEmoteTimelineItemFromMessage(message, isOutgoing, showSenderDetails, displayName, avatarImage)
|
||||
default:
|
||||
fatalError("Unknown room message.")
|
||||
}
|
||||
@@ -47,8 +47,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
_ isOutgoing: Bool,
|
||||
_ showSenderDetails: Bool,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol {
|
||||
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
|
||||
|
||||
return TextRoomTimelineItem(id: message.id,
|
||||
@@ -66,7 +66,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
_ isOutgoing: Bool,
|
||||
_ showSenderDetails: Bool,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol {
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
var aspectRatio: CGFloat?
|
||||
if let width = message.width,
|
||||
let height = message.height {
|
||||
@@ -93,8 +93,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
_ isOutgoing: Bool,
|
||||
_ showSenderDetails: Bool,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol {
|
||||
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
|
||||
|
||||
return NoticeRoomTimelineItem(id: message.id,
|
||||
@@ -112,8 +112,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
_ isOutgoing: Bool,
|
||||
_ showSenderDetails: Bool,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol {
|
||||
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
|
||||
|
||||
return EmoteRoomTimelineItem(id: message.id,
|
||||
|
||||
+1
-1
@@ -10,5 +10,5 @@ import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol RoomTimelineItemFactoryProtocol {
|
||||
func buildTimelineItemFor(message: RoomMessageProtocol, isOutgoing: Bool, showSenderDetails: Bool) -> RoomTimelineItemProtocol
|
||||
func buildTimelineItemFor(message: RoomMessageProtocol, isOutgoing: Bool, showSenderDetails: Bool) async -> RoomTimelineItemProtocol
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user