mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
* Fixes #276 - Rebuilt room timeline: - Removed the need for the ListCollectionViewAdapter - Rewrote the TimelineItemList without using introspection - Added ReversedScrollView for laying out items at the bottom/trailing - Rewrote TimelineProvider diffing through CollectionDifference (similar to the RoomSummaryProvider) - Added back `scrollDismissesKeyboard` behavior - Various other tweaks and fixes - Fixed various warnings: - removed async AttributedStringBuilder as AttributedString is non-sendable, made the RoomTimelineItemFactory synchronous - removed unused virtual timeline items - removed unused isOutgoing property from the FormattedBodyText * Make TimelineItemContextMenuActions indentifiable and specify contextMenu identifiers * Bump the matrix-rust-components-swift to v1.0.16-alpha * Add changes file and changelog contribution guide * Fix attributed string builder unit tests
This commit is contained in:
@@ -19,8 +19,6 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
private class WeakRoomSummaryProviderWrapper: SlidingSyncViewRoomListObserver, SlidingSyncViewStateObserver, SlidingSyncViewRoomsCountObserver {
|
||||
private weak var roomSummaryProvider: RoomSummaryProvider?
|
||||
|
||||
/// Publishes room list diffs as they come in through sliding sync
|
||||
let roomListDiffPublisher = PassthroughSubject<SlidingSyncViewRoomsListDiff, Never>()
|
||||
|
||||
@@ -29,11 +27,7 @@ private class WeakRoomSummaryProviderWrapper: SlidingSyncViewRoomListObserver, S
|
||||
|
||||
/// Publishes the number of available rooms
|
||||
let countUpdatePublisher = CurrentValueSubject<UInt, Never>(0)
|
||||
|
||||
init(roomSummaryProvider: RoomSummaryProvider) {
|
||||
self.roomSummaryProvider = roomSummaryProvider
|
||||
}
|
||||
|
||||
|
||||
// MARK: - SlidingSyncViewRoomListObserver
|
||||
|
||||
func didReceiveUpdate(diff: SlidingSyncViewRoomsListDiff) {
|
||||
@@ -85,7 +79,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
self.slidingSyncController = slidingSyncController
|
||||
self.roomMessageFactory = roomMessageFactory
|
||||
|
||||
let weakProvider = WeakRoomSummaryProviderWrapper(roomSummaryProvider: self)
|
||||
let weakProvider = WeakRoomSummaryProviderWrapper()
|
||||
|
||||
weakProvider.stateUpdatePublisher
|
||||
.map(RoomSummaryProviderState.init)
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
import Combine
|
||||
|
||||
struct MockRoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
var callbacks = PassthroughSubject<RoomTimelineProviderCallback, Never>()
|
||||
var itemProxies = [TimelineItemProxy]()
|
||||
var itemsPublisher = CurrentValueSubject<[TimelineItemProxy], Never>([])
|
||||
|
||||
var backPaginationPublisher = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
private var itemProxies = [TimelineItemProxy]()
|
||||
|
||||
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineProviderError> {
|
||||
.failure(.failedPaginatingBackwards)
|
||||
|
||||
@@ -51,14 +51,23 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
self.roomProxy = roomProxy
|
||||
|
||||
self.timelineProvider
|
||||
.callbacks
|
||||
.itemsPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
.sink { [weak self] _ in
|
||||
guard let self else { return }
|
||||
|
||||
switch callback {
|
||||
case .updatedMessages:
|
||||
self.updateTimelineItems()
|
||||
self.updateTimelineItems()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
self.timelineProvider
|
||||
.backPaginationPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] value in
|
||||
if value {
|
||||
self?.callbacks.send(.startedBackPaginating)
|
||||
} else {
|
||||
self?.callbacks.send(.finishedBackPaginating)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -71,7 +80,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineControllerError> {
|
||||
switch await timelineProvider.paginateBackwards(count) {
|
||||
case .success:
|
||||
updateTimelineItems()
|
||||
return .success(())
|
||||
case .failure:
|
||||
return .failure(.generic)
|
||||
@@ -135,13 +143,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
private func asyncUpdateTimelineItems() async {
|
||||
var newTimelineItems = [RoomTimelineItemProtocol]()
|
||||
|
||||
for (index, itemProxy) in timelineProvider.itemProxies.enumerated() {
|
||||
for (index, itemProxy) in timelineProvider.itemsPublisher.value.enumerated() {
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
|
||||
let previousItemProxy = timelineProvider.itemProxies[safe: index - 1]
|
||||
let nextItemProxy = timelineProvider.itemProxies[safe: index + 1]
|
||||
let previousItemProxy = timelineProvider.itemsPublisher.value[safe: index - 1]
|
||||
let nextItemProxy = timelineProvider.itemsPublisher.value[safe: index + 1]
|
||||
|
||||
let inGroupState = inGroupState(for: itemProxy, previousItemProxy: previousItemProxy, nextItemProxy: nextItemProxy)
|
||||
|
||||
@@ -149,16 +157,9 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
case .event(let eventItem):
|
||||
guard eventItem.isMessage || eventItem.isRedacted else { break } // To be handled in the future
|
||||
|
||||
newTimelineItems.append(await timelineItemFactory.buildTimelineItemFor(eventItemProxy: eventItem,
|
||||
inGroupState: inGroupState))
|
||||
case .virtual:
|
||||
// case .virtual(let virtualItem):
|
||||
// newTimelineItems.append(SeparatorRoomTimelineItem(id: message.originServerTs.ISO8601Format(),
|
||||
// text: message.originServerTs.formatted(date: .complete, time: .omitted)))
|
||||
#warning("Fix the UUID or \"bad things will happen\"")
|
||||
newTimelineItems.append(SeparatorRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "The day before"))
|
||||
case .other:
|
||||
newTimelineItems.append(timelineItemFactory.buildTimelineItemFor(eventItemProxy: eventItem,
|
||||
inGroupState: inGroupState))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import Foundation
|
||||
enum RoomTimelineControllerCallback {
|
||||
case updatedTimelineItems
|
||||
case updatedTimelineItem(_ itemId: String)
|
||||
case startedBackPaginating
|
||||
case finishedBackPaginating
|
||||
}
|
||||
|
||||
enum RoomTimelineControllerError: Error {
|
||||
|
||||
@@ -17,16 +17,11 @@
|
||||
import Combine
|
||||
import MatrixRustSDK
|
||||
|
||||
#warning("Rename to RoomTimelineListener???")
|
||||
class WeakRoomTimelineProviderWrapper: TimelineListener {
|
||||
private weak var timelineProvider: RoomTimelineProvider?
|
||||
|
||||
init(timelineProvider: RoomTimelineProvider) {
|
||||
self.timelineProvider = timelineProvider
|
||||
}
|
||||
private class RoomTimelineListener: TimelineListener {
|
||||
let itemsUpdatePublisher = PassthroughSubject<TimelineDiff, Never>()
|
||||
|
||||
func onUpdate(update: TimelineDiff) {
|
||||
timelineProvider?.onUpdate(update: update)
|
||||
itemsUpdatePublisher.send(update)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,25 +29,49 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
private let roomProxy: RoomProxyProtocol
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
let callbacks = PassthroughSubject<RoomTimelineProviderCallback, Never>()
|
||||
let itemsPublisher = CurrentValueSubject<[TimelineItemProxy], Never>([])
|
||||
let backPaginationPublisher = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
private(set) var itemProxies: [TimelineItemProxy]
|
||||
private var itemProxies: [TimelineItemProxy] {
|
||||
didSet {
|
||||
itemsPublisher.send(itemProxies)
|
||||
|
||||
if backPaginationPublisher.value == true {
|
||||
backPaginationPublisher.send(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(roomProxy: RoomProxyProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
itemProxies = []
|
||||
|
||||
Task {
|
||||
await roomProxy.addTimelineListener(listener: WeakRoomTimelineProviderWrapper(timelineProvider: self))
|
||||
let roomTimelineListener = RoomTimelineListener()
|
||||
await roomProxy.addTimelineListener(listener: roomTimelineListener)
|
||||
|
||||
roomTimelineListener
|
||||
.itemsUpdatePublisher
|
||||
.collect(.byTime(DispatchQueue.global(qos: .background), 0.5))
|
||||
.sink { self.updateItemsWithDiffs($0) }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineProviderError> {
|
||||
// Set this back to false after actually updating the items or if failed
|
||||
backPaginationPublisher.send(true)
|
||||
|
||||
switch await roomProxy.paginateBackwards(count: count) {
|
||||
case .success:
|
||||
return .success(())
|
||||
case .failure(let error):
|
||||
if error == .noMoreMessagesToBackPaginate { return .failure(.noMoreMessagesToBackPaginate) }
|
||||
backPaginationPublisher.send(false)
|
||||
|
||||
if error == .noMoreMessagesToBackPaginate {
|
||||
return .failure(.noMoreMessagesToBackPaginate)
|
||||
}
|
||||
|
||||
return .failure(.failedPaginatingBackwards)
|
||||
}
|
||||
}
|
||||
@@ -74,74 +93,70 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
return .failure(.failedRedactingItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
// MARK: - TimelineListener
|
||||
|
||||
private extension RoomTimelineProvider {
|
||||
func onUpdate(update: TimelineDiff) {
|
||||
let change = update.change()
|
||||
MXLog.verbose("Change: \(change)")
|
||||
private func updateItemsWithDiffs(_ diffs: [TimelineDiff]) {
|
||||
itemProxies = diffs
|
||||
.compactMap(buildDiff)
|
||||
.reduce(itemProxies) { $0.applying($1) ?? $0 }
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
private func buildDiff(from diff: TimelineDiff) -> CollectionDifference<TimelineItemProxy>? {
|
||||
var changes = [CollectionDifference<TimelineItemProxy>.Change]()
|
||||
|
||||
switch change {
|
||||
case .replace:
|
||||
replaceItems(update.replace())
|
||||
case .insertAt:
|
||||
insertItem(update.insertAt())
|
||||
case .updateAt:
|
||||
updateItem(update.updateAt())
|
||||
case .removeAt:
|
||||
removeItem(at: update.removeAt())
|
||||
case .move:
|
||||
moveItem(update.move())
|
||||
switch diff.change() {
|
||||
case .push:
|
||||
pushItem(update.push())
|
||||
case .pop:
|
||||
popItem()
|
||||
if let item = diff.push() {
|
||||
let itemProxy = TimelineItemProxy(item: item)
|
||||
changes.append(.insert(offset: Int(itemProxies.count), element: itemProxy, associatedWith: nil))
|
||||
}
|
||||
case .updateAt:
|
||||
if let update = diff.updateAt() {
|
||||
let itemProxy = TimelineItemProxy(item: update.item)
|
||||
changes.append(.remove(offset: Int(update.index), element: itemProxy, associatedWith: nil))
|
||||
changes.append(.insert(offset: Int(update.index), element: itemProxy, associatedWith: nil))
|
||||
}
|
||||
case .insertAt:
|
||||
if let update = diff.insertAt() {
|
||||
let itemProxy = TimelineItemProxy(item: update.item)
|
||||
changes.append(.insert(offset: Int(update.index), element: itemProxy, associatedWith: nil))
|
||||
}
|
||||
case .move:
|
||||
if let update = diff.move() {
|
||||
let itemProxy = itemProxies[Int(update.oldIndex)]
|
||||
changes.append(.remove(offset: Int(update.oldIndex), element: itemProxy, associatedWith: nil))
|
||||
changes.append(.insert(offset: Int(update.newIndex), element: itemProxy, associatedWith: nil))
|
||||
}
|
||||
case .removeAt:
|
||||
if let index = diff.removeAt() {
|
||||
let itemProxy = itemProxies[Int(index)]
|
||||
changes.append(.remove(offset: Int(index), element: itemProxy, associatedWith: nil))
|
||||
}
|
||||
case .replace:
|
||||
if let items = diff.replace() {
|
||||
for (index, itemProxy) in itemProxies.enumerated() {
|
||||
changes.append(.remove(offset: index, element: itemProxy, associatedWith: nil))
|
||||
}
|
||||
|
||||
items
|
||||
.reversed()
|
||||
.map { TimelineItemProxy(item: $0) }
|
||||
.forEach { itemProxy in
|
||||
changes.append(.insert(offset: 0, element: itemProxy, associatedWith: nil))
|
||||
}
|
||||
}
|
||||
case .clear:
|
||||
clearAllItems()
|
||||
for (index, itemProxy) in itemProxies.enumerated() {
|
||||
changes.append(.remove(offset: index, element: itemProxy, associatedWith: nil))
|
||||
}
|
||||
case .pop:
|
||||
if let itemProxy = itemProxies.last {
|
||||
changes.append(.remove(offset: itemProxies.count - 1, element: itemProxy, associatedWith: nil))
|
||||
}
|
||||
}
|
||||
|
||||
callbacks.send(.updatedMessages)
|
||||
}
|
||||
|
||||
private func replaceItems(_ items: [MatrixRustSDK.TimelineItem]?) {
|
||||
guard let items else { return }
|
||||
itemProxies = items.map(TimelineItemProxy.init)
|
||||
}
|
||||
|
||||
private func insertItem(_ data: InsertAtData?) {
|
||||
guard let data else { return }
|
||||
let itemProxy = TimelineItemProxy(item: data.item)
|
||||
itemProxies.insert(itemProxy, at: Int(data.index))
|
||||
}
|
||||
|
||||
private func updateItem(_ data: UpdateAtData?) {
|
||||
guard let data else { return }
|
||||
let itemProxy = TimelineItemProxy(item: data.item)
|
||||
itemProxies[Int(data.index)] = itemProxy
|
||||
}
|
||||
|
||||
private func removeItem(at index: UInt32?) {
|
||||
guard let index else { return }
|
||||
itemProxies.remove(at: Int(index))
|
||||
}
|
||||
|
||||
private func moveItem(_ data: MoveData?) {
|
||||
guard let data else { return }
|
||||
itemProxies.move(fromOffsets: IndexSet(integer: Int(data.oldIndex)), toOffset: Int(data.newIndex))
|
||||
}
|
||||
|
||||
private func pushItem(_ item: MatrixRustSDK.TimelineItem?) {
|
||||
guard let item else { return }
|
||||
itemProxies.append(TimelineItemProxy(item: item))
|
||||
}
|
||||
|
||||
private func popItem() {
|
||||
itemProxies.removeLast()
|
||||
}
|
||||
|
||||
private func clearAllItems() {
|
||||
itemProxies.removeAll()
|
||||
return CollectionDifference(changes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,6 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
enum RoomTimelineProviderCallback {
|
||||
case updatedMessages
|
||||
}
|
||||
|
||||
enum RoomTimelineProviderError: Error {
|
||||
case noMoreMessagesToBackPaginate
|
||||
case failedPaginatingBackwards
|
||||
@@ -30,9 +26,9 @@ enum RoomTimelineProviderError: Error {
|
||||
}
|
||||
|
||||
protocol RoomTimelineProviderProtocol {
|
||||
var callbacks: PassthroughSubject<RoomTimelineProviderCallback, Never> { get }
|
||||
var itemsPublisher: CurrentValueSubject<[TimelineItemProxy], Never> { get }
|
||||
|
||||
var itemProxies: [TimelineItemProxy] { get }
|
||||
var backPaginationPublisher: CurrentValueSubject<Bool, Never> { get }
|
||||
|
||||
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineProviderError>
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
}
|
||||
|
||||
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
|
||||
inGroupState: TimelineItemInGroupState) async -> RoomTimelineItemProtocol {
|
||||
inGroupState: TimelineItemInGroupState) -> RoomTimelineItemProtocol {
|
||||
let displayName = roomProxy.displayNameForUserId(eventItemProxy.sender)
|
||||
let avatarURL = roomProxy.avatarURLStringForUserId(eventItemProxy.sender)
|
||||
let avatarImage = mediaProvider.imageFromURLString(avatarURL, avatarSize: .user(on: .timeline))
|
||||
@@ -51,18 +51,18 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
switch messageContent.msgtype() {
|
||||
case .text(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return await buildTextTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
|
||||
return buildTextTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
|
||||
case .image(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return await buildImageTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
|
||||
return buildImageTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
|
||||
case .notice(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return await buildNoticeTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
|
||||
return buildNoticeTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
|
||||
case .emote(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return await buildEmoteTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
|
||||
return buildEmoteTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
|
||||
case .none:
|
||||
return await buildFallbackTimelineItem(eventItemProxy, isOutgoing, inGroupState, displayName, avatarImage)
|
||||
return buildFallbackTimelineItem(eventItemProxy, isOutgoing, inGroupState, displayName, avatarImage)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
_ isOutgoing: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await attributedStringBuilder.fromPlain(eventItemProxy.body)
|
||||
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol {
|
||||
let attributedText = attributedStringBuilder.fromPlain(eventItemProxy.body)
|
||||
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
|
||||
|
||||
return TextRoomTimelineItem(id: eventItemProxy.id,
|
||||
@@ -109,8 +109,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
_ isOutgoing: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol {
|
||||
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
|
||||
|
||||
return TextRoomTimelineItem(id: message.id,
|
||||
@@ -130,7 +130,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
_ isOutgoing: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol {
|
||||
var aspectRatio: CGFloat?
|
||||
if let width = message.width,
|
||||
let height = message.height {
|
||||
@@ -159,8 +159,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
_ isOutgoing: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol {
|
||||
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
|
||||
|
||||
return NoticeRoomTimelineItem(id: message.id,
|
||||
@@ -180,8 +180,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
_ isOutgoing: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) async -> RoomTimelineItemProtocol {
|
||||
let attributedText = await (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol {
|
||||
let attributedText = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||
let attributedComponents = attributedStringBuilder.blockquoteCoalescedComponentsFrom(attributedText)
|
||||
|
||||
return EmoteRoomTimelineItem(id: message.id,
|
||||
|
||||
+1
-1
@@ -19,5 +19,5 @@ import Foundation
|
||||
@MainActor
|
||||
protocol RoomTimelineItemFactoryProtocol {
|
||||
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
|
||||
inGroupState: TimelineItemInGroupState) async -> RoomTimelineItemProtocol
|
||||
inGroupState: TimelineItemInGroupState) -> RoomTimelineItemProtocol
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user