Made room state event collapsing configurable, expose it in the developer menu. Allow the UserSetting property wrappers to only store values in memory (+11 squashed commits)

Squashed commits:
[42e45fc] Even more tweaks following code review
[5dcd5be] Add swift-algoritms and switch bubbling detection to its `chunked` method
[4ac70ed] Move the groupBy implementation to Collection instead of Array
[6aeffc3] Tweaks following code review
[0ca5ac2] Bubbles working again, grouping computed closer to the UI level
[3a66030] Refactor how timeline items are built in the RoomTimelineController
[57d51e9] Remove grouping from timeline items
[8608950] Remove the RoomTimelineViewFactory, update the GroupRoomTimelineView
[9e52e61] Add array grouping extension, unit tests and switched the timeline controller to it
[7d213a1] First attempt
[90bb1d7] Remove now unused `RoomTimelineController` `updatedTimelineItem` calback
This commit is contained in:
Stefan Ceriu
2023-02-15 17:14:50 +02:00
committed by Stefan Ceriu
parent 64902190d4
commit a3c7c47a69
58 changed files with 679 additions and 458 deletions
@@ -23,7 +23,6 @@ enum RoomTimelineItemFixtures {
TextRoomTimelineItem(id: UUID().uuidString,
body: "That looks so good!",
timestamp: "10:10 AM",
groupState: .single,
isOutgoing: false,
isEditable: false,
sender: .init(id: "", displayName: "Jacob"),
@@ -31,7 +30,6 @@ enum RoomTimelineItemFixtures {
TextRoomTimelineItem(id: UUID().uuidString,
body: "Let’s get lunch soon! New salad place opened up 🥗. When are y’all free? 🤗",
timestamp: "10:11 AM",
groupState: .beginning,
isOutgoing: false,
isEditable: false,
sender: .init(id: "", displayName: "Helena"),
@@ -41,7 +39,6 @@ enum RoomTimelineItemFixtures {
TextRoomTimelineItem(id: UUID().uuidString,
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,
isEditable: false,
sender: .init(id: "", displayName: "Helena"),
@@ -53,21 +50,18 @@ enum RoomTimelineItemFixtures {
TextRoomTimelineItem(id: UUID().uuidString,
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,
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,
body: "New home office set up!",
timestamp: "5 PM",
groupState: .end,
isOutgoing: true,
isEditable: true,
sender: .init(id: "", displayName: "Bob"),
@@ -79,7 +73,6 @@ enum RoomTimelineItemFixtures {
body: "",
formattedBody: AttributedStringBuilder().fromHTML("Hol' up <blockquote>New home office set up!</blockquote>That's amazing! Congrats 🥳"),
timestamp: "5 PM",
groupState: .single,
isOutgoing: false,
isEditable: false,
sender: .init(id: "", displayName: "Helena"))
@@ -88,24 +81,20 @@ enum RoomTimelineItemFixtures {
/// A small chunk of events, containing 2 text items.
static var smallChunk: [RoomTimelineItemProtocol] {
[TextRoomTimelineItem(text: "Hey there 👋",
groupState: .beginning,
senderDisplayName: "Alice"),
TextRoomTimelineItem(text: "How are you?",
groupState: .end,
senderDisplayName: "Alice")]
}
/// A chunk of events that contains a single text item.
static var singleMessageChunk: [RoomTimelineItemProtocol] {
[TextRoomTimelineItem(text: "Tap tap tap 🎙️. Is this thing on?",
groupState: .single,
senderDisplayName: "Helena")]
}
/// A single text item.
static var incomingMessage: RoomTimelineItemProtocol {
TextRoomTimelineItem(text: "Hello, World!",
groupState: .single,
senderDisplayName: "Bob")
}
@@ -196,11 +185,10 @@ enum RoomTimelineItemFixtures {
}
private extension TextRoomTimelineItem {
init(text: String, groupState: TimelineItemGroupState = .single, senderDisplayName: String) {
init(text: String, senderDisplayName: String) {
self.init(id: UUID().uuidString,
body: text,
timestamp: "10:47 am",
groupState: groupState,
isOutgoing: senderDisplayName == "Alice",
isEditable: false,
sender: .init(id: "", displayName: senderDisplayName))
@@ -230,61 +230,30 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
updateTimelineItems()
}
// swiftlint:disable:next cyclomatic_complexity
private func updateTimelineItems() {
var newTimelineItems = [RoomTimelineItemProtocol]()
var canBackPaginate = true
var isBackPaginating = false
var createdIdentifiers = [String: Bool]()
for (index, itemProxy) in timelineProvider.itemsPublisher.value.enumerated() {
if Task.isCancelled {
return
}
let previousItemProxy = timelineProvider.itemsPublisher.value[safe: index - 1]
let nextItemProxy = timelineProvider.itemsPublisher.value[safe: index + 1]
let groupState = computeGroupState(for: itemProxy, previousItemProxy: previousItemProxy, nextItemProxy: nextItemProxy)
switch itemProxy {
case .event(let eventItemProxy):
if let timelineItem = timelineItemFactory.buildTimelineItemFor(eventItemProxy: eventItemProxy, groupState: groupState) {
#warning("This works around duplicated items coming out of the SDK, remove once fixed")
if createdIdentifiers[timelineItem.id] == nil {
newTimelineItems.append(timelineItem)
createdIdentifiers[timelineItem.id] = true
} else {
MXLog.error("Found duplicated timeline item, ignoring")
}
}
case .virtual(let virtualItem):
switch virtualItem {
case .dayDivider(let timestamp):
// These components will be replaced by a timestamp in upcoming releases
let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
let dateString = date.formatted(date: .complete, time: .omitted)
newTimelineItems.append(SeparatorRoomTimelineItem(text: dateString))
case .readMarker:
// Don't show the read marker if it's the last item in the timeline
if index != timelineProvider.itemsPublisher.value.indices.last {
newTimelineItems.append(ReadMarkerRoomTimelineItem())
}
case .loadingIndicator:
newTimelineItems.append(PaginationIndicatorRoomTimelineItem())
isBackPaginating = true
case .timelineStart:
newTimelineItems.append(TimelineStartRoomTimelineItem(name: roomProxy.displayName ?? roomProxy.name))
canBackPaginate = false
}
default:
break
}
}
if Task.isCancelled {
return
let collapsibleChunks = timelineProvider.itemsPublisher.value.groupBy { isItemCollapsible($0) }
for (index, itemGroup) in collapsibleChunks.enumerated() {
if itemGroup.count == 1, let itemProxy = itemGroup.first {
let isLastItem = index == collapsibleChunks.indices.last
if let item = buildTimelineItemFor(itemProxy: itemProxy, isLastItem: isLastItem, createdIdentifiers: &createdIdentifiers, isBackPaginating: &isBackPaginating, canBackPaginate: &canBackPaginate) {
newTimelineItems.append(item)
}
} else {
let items = itemGroup.compactMap { itemProxy in
buildTimelineItemFor(itemProxy: itemProxy, isLastItem: false, createdIdentifiers: &createdIdentifiers, isBackPaginating: &isBackPaginating, canBackPaginate: &canBackPaginate)
}
if !items.isEmpty {
newTimelineItems.append(CollapsibleTimelineItem(items: items))
}
}
}
timelineItems = newTimelineItems
@@ -294,49 +263,63 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
callbacks.send(.isBackPaginating(isBackPaginating))
}
private func computeGroupState(for itemProxy: TimelineItemProxy,
previousItemProxy: TimelineItemProxy?,
nextItemProxy: TimelineItemProxy?) -> TimelineItemGroupState {
guard let previousItem = previousItemProxy else {
// no previous item, check next item
guard let nextItem = nextItemProxy else {
// no next item neither, this is single
return .single
private func buildTimelineItemFor(itemProxy: TimelineItemProxy,
isLastItem: Bool,
createdIdentifiers: inout [String: Bool],
isBackPaginating: inout Bool,
canBackPaginate: inout Bool) -> RoomTimelineItemProtocol? {
switch itemProxy {
case .event(let eventItemProxy):
if let timelineItem = timelineItemFactory.buildTimelineItemFor(eventItemProxy: eventItemProxy) {
#warning("This works around duplicated items coming out of the SDK, remove once fixed")
if createdIdentifiers[timelineItem.id] == nil {
createdIdentifiers[timelineItem.id] = true
return timelineItem
} else {
MXLog.error("Found duplicated timeline item, ignoring")
}
}
guard nextItem.canBeGrouped(with: itemProxy) else {
// there is a next item but can't be grouped, this is single
return .single
case .virtual(let virtualItem):
switch virtualItem {
case .dayDivider(let timestamp):
// These components will be replaced by a timestamp in upcoming releases
let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
let dateString = date.formatted(date: .complete, time: .omitted)
return SeparatorRoomTimelineItem(text: dateString)
case .readMarker:
// Don't show the read marker if it's the last item in the timeline
if !isLastItem {
return ReadMarkerRoomTimelineItem()
}
case .loadingIndicator:
isBackPaginating = true
return PaginationIndicatorRoomTimelineItem()
case .timelineStart:
canBackPaginate = false
return TimelineStartRoomTimelineItem(name: roomProxy.displayName ?? roomProxy.name)
}
// next will be grouped with this one, this is the start
return .beginning
case .unknown:
return nil
}
guard let nextItem = nextItemProxy else {
// no next item
guard itemProxy.canBeGrouped(with: previousItem) else {
// there is a previous item but can't be grouped, this is single
return .single
return nil
}
private func isItemCollapsible(_ item: TimelineItemProxy) -> Bool {
if !ServiceLocator.shared.settings.shouldCollapseRoomStateEvents {
return false
}
if case let .event(eventItem) = item {
switch eventItem.content.kind() {
case .profileChange, .roomMembership, .state:
return true
default:
return false
}
// will be grouped with previous, this is the end
return .end
}
guard itemProxy.canBeGrouped(with: previousItem) else {
guard nextItem.canBeGrouped(with: itemProxy) else {
// there is a next item but can't be grouped, this is single
return .single
}
// next will be grouped with this one, this is the start
return .beginning
}
guard nextItem.canBeGrouped(with: itemProxy) else {
// there is a next item but can't be grouped, this is the end
return .end
}
// next will be grouped with this one, this is the start
return .middle
return false
}
private func loadVideoForTimelineItem(_ timelineItem: VideoRoomTimelineItem) async {
@@ -20,7 +20,6 @@ import UIKit
enum RoomTimelineControllerCallback {
case updatedTimelineItems
case updatedTimelineItem(_ itemId: String)
case canBackPaginate(Bool)
case isBackPaginating(Bool)
}
@@ -32,20 +32,6 @@ enum TimelineItemProxy {
self = .unknown(item)
}
}
func canBeGrouped(with previousItemProxy: TimelineItemProxy) -> Bool {
guard case let .event(selfEventItemProxy) = self, case let .event(previousEventItemProxy) = previousItemProxy else {
return false
}
// State events aren't rendered as messages so shouldn't be grouped.
if selfEventItemProxy.isRoomState || previousEventItemProxy.isRoomState {
return false
}
// can be improved by adding a date threshold
return previousEventItemProxy.reactions.isEmpty && selfEventItemProxy.sender == previousEventItemProxy.sender
}
}
/// The delivery status for the item.
@@ -17,27 +17,9 @@
import Foundation
import UIKit
enum TimelineItemGroupState: Hashable {
case single
case beginning
case middle
case end
var shouldShowSenderDetails: Bool {
switch self {
case .single, .beginning:
return true
default:
return false
}
}
}
protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol, CustomStringConvertible {
var body: String { get }
var timestamp: String { get }
var shouldShowSenderDetails: Bool { get }
var groupState: TimelineItemGroupState { get }
var isOutgoing: Bool { get }
var isEditable: Bool { get }
@@ -47,31 +29,6 @@ protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol, CustomStringC
}
extension EventBasedTimelineItemProtocol {
var shouldShowSenderDetails: Bool {
groupState.shouldShowSenderDetails
}
var roundedCorners: UIRectCorner {
switch groupState {
case .single:
return .allCorners
case .beginning:
if isOutgoing {
return [.topLeft, .topRight, .bottomLeft]
} else {
return [.topLeft, .topRight, .bottomRight]
}
case .middle:
return isOutgoing ? [.topLeft, .bottomLeft] : [.topRight, .bottomRight]
case .end:
if isOutgoing {
return [.topLeft, .bottomLeft, .bottomRight]
} else {
return [.topRight, .bottomLeft, .bottomRight]
}
}
}
var description: String {
"\(String(describing: Self.self)): id: \(id), timestamp: \(timestamp), isOutgoing: \(isOutgoing), properties: \(properties)"
}
@@ -20,7 +20,6 @@ struct AudioRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
let id: String
let body: String
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
let isEditable: Bool
let sender: TimelineItemSender
@@ -21,7 +21,6 @@ struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
let body: String
var formattedBody: AttributedString?
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
let isEditable: Bool
@@ -20,7 +20,6 @@ struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hasha
let id: String
let body: String
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
let isEditable: Bool
@@ -21,7 +21,6 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
let id: String
let body: String
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
let isEditable: Bool
@@ -21,7 +21,6 @@ struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Has
let body: String
var formattedBody: AttributedString?
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
let isEditable: Bool
@@ -21,7 +21,6 @@ struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hasha
let body: String
var formattedBody: AttributedString?
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
let isEditable: Bool
@@ -20,7 +20,6 @@ struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
let id: String
let body: String
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
let isEditable: Bool
@@ -0,0 +1,43 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
struct CollapsibleTimelineItem: RoomTimelineItemProtocol, Identifiable, Hashable {
let id: String
let items: [RoomTimelineItemProtocol]
init(items: [RoomTimelineItemProtocol]) {
guard let firstItem = items.first else {
fatalError()
}
self.items = items
id = firstItem.id
}
// MARK: - Equatable
static func == (lhs: CollapsibleTimelineItem, rhs: CollapsibleTimelineItem) -> Bool {
lhs.id == rhs.id
}
// MARK: - Hashable
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
@@ -27,7 +27,6 @@ struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable,
let body: String
let encryptionType: EncryptionType
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
let isEditable: Bool
@@ -21,7 +21,6 @@ struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, H
let id: String
let body: String
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
let isEditable: Bool
@@ -20,7 +20,6 @@ struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
let id: String
let body: String
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
let isEditable: Bool
@@ -20,7 +20,6 @@ struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Ha
let id: String
let body: String
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
let isEditable: Bool
@@ -24,7 +24,6 @@ struct UnsupportedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable
let error: String
let timestamp: String
let groupState: TimelineItemGroupState
let isOutgoing: Bool
let isEditable: Bool
@@ -36,53 +36,52 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
}
// swiftlint:disable:next cyclomatic_complexity
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol? {
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy) -> RoomTimelineItemProtocol? {
let isOutgoing = eventItemProxy.isOwn
switch eventItemProxy.content.kind() {
case .unableToDecrypt(let encryptedMessage):
return buildEncryptedTimelineItem(eventItemProxy, encryptedMessage, isOutgoing, groupState)
return buildEncryptedTimelineItem(eventItemProxy, encryptedMessage, isOutgoing)
case .redactedMessage:
return buildRedactedTimelineItem(eventItemProxy, isOutgoing, groupState)
return buildRedactedTimelineItem(eventItemProxy, isOutgoing)
case .sticker(let body, let imageInfo, let urlString):
guard let url = URL(string: urlString) else {
MXLog.error("Invalid sticker url string: \(urlString)")
return buildUnsupportedTimelineItem(eventItemProxy, "m.sticker", "Invalid Sticker URL", isOutgoing, groupState)
return buildUnsupportedTimelineItem(eventItemProxy, "m.sticker", "Invalid Sticker URL", isOutgoing)
}
return buildStickerTimelineItem(eventItemProxy, body, imageInfo, url, isOutgoing, groupState)
return buildStickerTimelineItem(eventItemProxy, body, imageInfo, url, isOutgoing)
case .failedToParseMessageLike(let eventType, let error):
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing, groupState)
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing)
case .failedToParseState(let eventType, _, let error):
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing, groupState)
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing)
case .message:
guard let messageContent = eventItemProxy.content.asMessage() else { fatalError("Invalid message timeline item: \(eventItemProxy)") }
switch messageContent.msgtype() {
case .text(content: let content):
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
return buildTextTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
return buildTextTimelineItemFromMessage(eventItemProxy, message, isOutgoing)
case .image(content: let content):
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
return buildImageTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
return buildImageTimelineItemFromMessage(eventItemProxy, message, isOutgoing)
case .video(let content):
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
return buildVideoTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
return buildVideoTimelineItemFromMessage(eventItemProxy, message, isOutgoing)
case .file(let content):
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
return buildFileTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
case .notice(let content):
return buildFileTimelineItemFromMessage(eventItemProxy, message, isOutgoing)
case .notice(content: let content):
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
return buildNoticeTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
case .emote(let content):
return buildNoticeTimelineItemFromMessage(eventItemProxy, message, isOutgoing)
case .emote(content: let content):
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
return buildEmoteTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
return buildEmoteTimelineItemFromMessage(eventItemProxy, message, isOutgoing)
case .audio(let content):
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
return buildAudioTimelineItem(eventItemProxy, message, isOutgoing, groupState)
return buildAudioTimelineItem(eventItemProxy, message, isOutgoing)
case .none:
return buildFallbackTimelineItem(eventItemProxy, isOutgoing, groupState)
return buildFallbackTimelineItem(eventItemProxy, isOutgoing)
}
case .state(let stateKey, let content):
return buildStateTimelineItemFor(eventItemProxy: eventItemProxy, state: content, stateKey: stateKey, isOutgoing: isOutgoing)
@@ -98,27 +97,23 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildUnsupportedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ eventType: String,
_ error: String,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
UnsupportedRoomTimelineItem(id: eventItemProxy.id,
body: ElementL10n.roomTimelineItemUnsupported,
eventType: eventType,
error: error,
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
sender: eventItemProxy.sender,
properties: RoomTimelineItemProperties())
}
// swiftlint:disable:next function_parameter_count
private func buildStickerTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ body: String,
_ imageInfo: ImageInfo,
_ imageURL: URL,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
var aspectRatio: CGFloat?
let width = imageInfo.width.map(CGFloat.init)
let height = imageInfo.height.map(CGFloat.init)
@@ -129,7 +124,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
return StickerRoomTimelineItem(id: eventItemProxy.id,
body: body,
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
sender: eventItemProxy.sender,
@@ -144,8 +138,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ encryptedMessage: EncryptedMessage,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
var encryptionType = EncryptedRoomTimelineItem.EncryptionType.unknown
switch encryptedMessage {
case .megolmV1AesSha2(let sessionId):
@@ -160,7 +153,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
body: ElementL10n.roomTimelineUnableToDecrypt,
encryptionType: encryptionType,
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
sender: eventItemProxy.sender,
@@ -168,12 +160,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
}
private func buildRedactedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
RedactedRoomTimelineItem(id: eventItemProxy.id,
body: ElementL10n.eventRedacted,
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
sender: eventItemProxy.sender,
@@ -181,15 +171,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
}
private func buildFallbackTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
let formattedBody = attributedStringBuilder.fromPlain(eventItemProxy.body)
return TextRoomTimelineItem(id: eventItemProxy.id,
body: eventItemProxy.body ?? "",
formattedBody: formattedBody,
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
sender: eventItemProxy.sender,
@@ -199,15 +187,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildTextTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
_ message: MessageTimelineItem<TextMessageContent>,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
let formattedBody = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
return TextRoomTimelineItem(id: message.id,
body: message.body,
formattedBody: formattedBody,
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
sender: eventItemProxy.sender,
@@ -218,8 +204,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildImageTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
_ message: MessageTimelineItem<ImageMessageContent>,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
var aspectRatio: CGFloat?
if let width = message.width,
let height = message.height {
@@ -229,7 +214,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
return ImageRoomTimelineItem(id: message.id,
body: message.body,
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
sender: eventItemProxy.sender,
@@ -246,8 +230,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildVideoTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
_ message: MessageTimelineItem<VideoMessageContent>,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
var aspectRatio: CGFloat?
if let width = message.width,
let height = message.height {
@@ -257,7 +240,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
return VideoRoomTimelineItem(id: message.id,
body: message.body,
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
sender: eventItemProxy.sender,
@@ -275,12 +257,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildAudioTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
_ message: MessageTimelineItem<AudioMessageContent>,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
AudioRoomTimelineItem(id: eventItemProxy.id,
body: message.body,
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
sender: eventItemProxy.sender,
@@ -290,12 +270,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildFileTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
_ message: MessageTimelineItem<FileMessageContent>,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
FileRoomTimelineItem(id: message.id,
body: message.body,
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
sender: eventItemProxy.sender,
@@ -308,15 +286,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildNoticeTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
_ message: MessageTimelineItem<NoticeMessageContent>,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
let formattedBody = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
return NoticeRoomTimelineItem(id: message.id,
body: message.body,
formattedBody: formattedBody,
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
sender: eventItemProxy.sender,
@@ -327,8 +303,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private func buildEmoteTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
_ message: MessageTimelineItem<EmoteMessageContent>,
_ isOutgoing: Bool,
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
let name = eventItemProxy.sender.displayName ?? eventItemProxy.sender.id
var formattedBody: AttributedString?
@@ -342,7 +317,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
body: message.body,
formattedBody: formattedBody,
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
groupState: groupState,
isOutgoing: isOutgoing,
isEditable: eventItemProxy.isEditable,
sender: eventItemProxy.sender,
@@ -396,7 +370,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
StateRoomTimelineItem(id: eventItemProxy.id,
body: text,
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
groupState: .single,
isOutgoing: isOutgoing,
isEditable: false,
sender: eventItemProxy.sender)
@@ -18,6 +18,5 @@ import Foundation
@MainActor
protocol RoomTimelineItemFactoryProtocol {
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol?
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy) -> RoomTimelineItemProtocol?
}
@@ -1,59 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
struct RoomTimelineViewFactory: RoomTimelineViewFactoryProtocol {
// swiftlint:disable:next cyclomatic_complexity
func buildTimelineViewFor(timelineItem: RoomTimelineItemProtocol) -> RoomTimelineViewProvider {
switch timelineItem {
case let item as TextRoomTimelineItem:
return .text(item)
case let item as ImageRoomTimelineItem:
return .image(item)
case let item as VideoRoomTimelineItem:
return .video(item)
case let item as AudioRoomTimelineItem:
return .audio(item)
case let item as FileRoomTimelineItem:
return .file(item)
case let item as SeparatorRoomTimelineItem:
return .separator(item)
case let item as NoticeRoomTimelineItem:
return .notice(item)
case let item as EmoteRoomTimelineItem:
return .emote(item)
case let item as RedactedRoomTimelineItem:
return .redacted(item)
case let item as EncryptedRoomTimelineItem:
return .encrypted(item)
case let item as ReadMarkerRoomTimelineItem:
return .readMarker(item)
case let item as PaginationIndicatorRoomTimelineItem:
return .paginationIndicator(item)
case let item as StickerRoomTimelineItem:
return .sticker(item)
case let item as UnsupportedRoomTimelineItem:
return .unsupported(item)
case let item as TimelineStartRoomTimelineItem:
return .timelineStart(item)
case let item as StateRoomTimelineItem:
return .state(item)
default:
fatalError("Unknown timeline item")
}
}
}
@@ -1,22 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
@MainActor
protocol RoomTimelineViewFactoryProtocol {
func buildTimelineViewFor(timelineItem: RoomTimelineItemProtocol) -> RoomTimelineViewProvider
}
@@ -18,56 +18,85 @@ import Foundation
import SwiftUI
enum RoomTimelineViewProvider: Identifiable, Hashable {
case text(TextRoomTimelineItem)
case separator(SeparatorRoomTimelineItem)
case image(ImageRoomTimelineItem)
case video(VideoRoomTimelineItem)
case audio(AudioRoomTimelineItem)
case file(FileRoomTimelineItem)
case emote(EmoteRoomTimelineItem)
case notice(NoticeRoomTimelineItem)
case redacted(RedactedRoomTimelineItem)
case encrypted(EncryptedRoomTimelineItem)
case readMarker(ReadMarkerRoomTimelineItem)
case paginationIndicator(PaginationIndicatorRoomTimelineItem)
case sticker(StickerRoomTimelineItem)
case unsupported(UnsupportedRoomTimelineItem)
case timelineStart(TimelineStartRoomTimelineItem)
case state(StateRoomTimelineItem)
case text(TextRoomTimelineItem, TimelineGroupStyle)
case separator(SeparatorRoomTimelineItem, TimelineGroupStyle)
case image(ImageRoomTimelineItem, TimelineGroupStyle)
case video(VideoRoomTimelineItem, TimelineGroupStyle)
case audio(AudioRoomTimelineItem, TimelineGroupStyle)
case file(FileRoomTimelineItem, TimelineGroupStyle)
case emote(EmoteRoomTimelineItem, TimelineGroupStyle)
case notice(NoticeRoomTimelineItem, TimelineGroupStyle)
case redacted(RedactedRoomTimelineItem, TimelineGroupStyle)
case encrypted(EncryptedRoomTimelineItem, TimelineGroupStyle)
case readMarker(ReadMarkerRoomTimelineItem, TimelineGroupStyle)
case paginationIndicator(PaginationIndicatorRoomTimelineItem, TimelineGroupStyle)
case sticker(StickerRoomTimelineItem, TimelineGroupStyle)
case unsupported(UnsupportedRoomTimelineItem, TimelineGroupStyle)
case timelineStart(TimelineStartRoomTimelineItem, TimelineGroupStyle)
case state(StateRoomTimelineItem, TimelineGroupStyle)
case group(CollapsibleTimelineItem, TimelineGroupStyle)
// swiftlint:disable:next cyclomatic_complexity
init(timelineItem: RoomTimelineItemProtocol, grouping: TimelineGroupStyle) {
switch timelineItem {
case let item as TextRoomTimelineItem:
self = .text(item, grouping)
case let item as ImageRoomTimelineItem:
self = .image(item, grouping)
case let item as VideoRoomTimelineItem:
self = .video(item, grouping)
case let item as AudioRoomTimelineItem:
self = .audio(item, grouping)
case let item as FileRoomTimelineItem:
self = .file(item, grouping)
case let item as SeparatorRoomTimelineItem:
self = .separator(item, grouping)
case let item as NoticeRoomTimelineItem:
self = .notice(item, grouping)
case let item as EmoteRoomTimelineItem:
self = .emote(item, grouping)
case let item as RedactedRoomTimelineItem:
self = .redacted(item, grouping)
case let item as EncryptedRoomTimelineItem:
self = .encrypted(item, grouping)
case let item as ReadMarkerRoomTimelineItem:
self = .readMarker(item, grouping)
case let item as PaginationIndicatorRoomTimelineItem:
self = .paginationIndicator(item, grouping)
case let item as StickerRoomTimelineItem:
self = .sticker(item, grouping)
case let item as UnsupportedRoomTimelineItem:
self = .unsupported(item, grouping)
case let item as TimelineStartRoomTimelineItem:
self = .timelineStart(item, grouping)
case let item as StateRoomTimelineItem:
self = .state(item, grouping)
case let item as CollapsibleTimelineItem:
self = .group(item, grouping)
default:
fatalError("Unknown timeline item")
}
}
var id: String {
switch self {
case .text(let item):
return item.id
case .separator(let item):
return item.id
case .image(let item):
return item.id
case .video(let item):
return item.id
case .audio(let item):
return item.id
case .file(let item):
return item.id
case .emote(let item):
return item.id
case .notice(let item):
return item.id
case .redacted(let item):
return item.id
case .encrypted(let item):
return item.id
case .readMarker(let item):
return item.id
case .paginationIndicator(let item):
return item.id
case .sticker(let item):
return item.id
case .unsupported(let item):
return item.id
case .timelineStart(let item):
return item.id
case .state(let item):
case .text(let item as RoomTimelineItemProtocol, _),
.separator(let item as RoomTimelineItemProtocol, _),
.image(let item as RoomTimelineItemProtocol, _),
.video(let item as RoomTimelineItemProtocol, _),
.audio(let item as RoomTimelineItemProtocol, _),
.file(let item as RoomTimelineItemProtocol, _),
.emote(let item as RoomTimelineItemProtocol, _),
.notice(let item as RoomTimelineItemProtocol, _),
.redacted(let item as RoomTimelineItemProtocol, _),
.encrypted(let item as RoomTimelineItemProtocol, _),
.readMarker(let item as RoomTimelineItemProtocol, _),
.paginationIndicator(let item as RoomTimelineItemProtocol, _),
.sticker(let item as RoomTimelineItemProtocol, _),
.unsupported(let item as RoomTimelineItemProtocol, _),
.timelineStart(let item as RoomTimelineItemProtocol, _),
.state(let item as RoomTimelineItemProtocol, _),
.group(let item as RoomTimelineItemProtocol, _):
return item.id
}
}
@@ -81,45 +110,77 @@ enum RoomTimelineViewProvider: Identifiable, Hashable {
return false
case .timelineStart, .separator, .readMarker, .paginationIndicator: // Virtual items are never reactable
return false
case .group:
return false
}
}
}
extension RoomTimelineViewProvider: View {
@ViewBuilder var body: some View {
var body: some View {
timelineView
.environment(\.timelineGroupStyle, timelineStyleGrouping)
}
@ViewBuilder private var timelineView: some View {
switch self {
case .text(let item):
case .text(let item, _):
TextRoomTimelineView(timelineItem: item)
case .separator(let item):
case .separator(let item, _):
SeparatorRoomTimelineView(timelineItem: item)
case .image(let item):
case .image(let item, _):
ImageRoomTimelineView(timelineItem: item)
case .video(let item):
case .video(let item, _):
VideoRoomTimelineView(timelineItem: item)
case .audio(let item):
case .audio(let item, _):
AudioRoomTimelineView(timelineItem: item)
case .file(let item):
case .file(let item, _):
FileRoomTimelineView(timelineItem: item)
case .emote(let item):
case .emote(let item, _):
EmoteRoomTimelineView(timelineItem: item)
case .notice(let item):
case .notice(let item, _):
NoticeRoomTimelineView(timelineItem: item)
case .redacted(let item):
case .redacted(let item, _):
RedactedRoomTimelineView(timelineItem: item)
case .encrypted(let item):
case .encrypted(let item, _):
EncryptedRoomTimelineView(timelineItem: item)
case .readMarker(let item):
case .readMarker(let item, _):
ReadMarkerRoomTimelineView(timelineItem: item)
case .paginationIndicator(let item):
case .paginationIndicator(let item, _):
PaginationIndicatorRoomTimelineView(timelineItem: item)
case .sticker(let item):
case .sticker(let item, _):
StickerRoomTimelineView(timelineItem: item)
case .unsupported(let item):
case .unsupported(let item, _):
UnsupportedRoomTimelineView(timelineItem: item)
case .timelineStart(let item):
case .timelineStart(let item, _):
TimelineStartRoomTimelineView(timelineItem: item)
case .state(let item):
case .state(let item, _):
StateRoomTimelineView(timelineItem: item)
case .group(let item, _):
CollapsibleRoomTimelineView(timelineItem: item)
}
}
private var timelineStyleGrouping: TimelineGroupStyle {
switch self {
case .text(_, let grouping),
.separator(_, let grouping),
.image(_, let grouping),
.video(_, let grouping),
.audio(_, let grouping),
.file(_, let grouping),
.emote(_, let grouping),
.notice(_, let grouping),
.redacted(_, let grouping),
.encrypted(_, let grouping),
.readMarker(_, let grouping),
.paginationIndicator(_, let grouping),
.sticker(_, let grouping),
.unsupported(_, let grouping),
.timelineStart(_, let grouping),
.state(_, let grouping),
.group(_, let grouping):
return grouping
}
}
}