mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
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:
committed by
Stefan Ceriu
parent
64902190d4
commit
a3c7c47a69
@@ -117,6 +117,15 @@
|
|||||||
"version" : "7.30.2"
|
"version" : "7.30.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-algorithms",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-algorithms",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e",
|
||||||
|
"version" : "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-collections",
|
"identity" : "swift-collections",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -126,6 +135,15 @@
|
|||||||
"version" : "1.0.4"
|
"version" : "1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-numerics",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-numerics",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "0a5bc04095a675662cf24757cc0640aa2204253b",
|
||||||
|
"version" : "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-snapshot-testing",
|
"identity" : "swift-snapshot-testing",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
"room_timeline_image_gif" = "GIF";
|
"room_timeline_image_gif" = "GIF";
|
||||||
"room_timeline_read_marker_title" = "New";
|
"room_timeline_read_marker_title" = "New";
|
||||||
|
|
||||||
|
"room_timeline_state_changes" = "%d room changes";
|
||||||
|
|
||||||
"noticeRoomInviteAccepted" = "%1$@ accepted the invite";
|
"noticeRoomInviteAccepted" = "%1$@ accepted the invite";
|
||||||
"noticeRoomInviteAcceptedByYou" = "You accepted the invite";
|
"noticeRoomInviteAcceptedByYou" = "You accepted the invite";
|
||||||
"noticeRoomKnock" = "%1$@ requested to join";
|
"noticeRoomKnock" = "%1$@ requested to join";
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ final class AppSettings: ObservableObject {
|
|||||||
case isIdentifiedForAnalytics
|
case isIdentifiedForAnalytics
|
||||||
case enableInAppNotifications
|
case enableInAppNotifications
|
||||||
case pusherProfileTag
|
case pusherProfileTag
|
||||||
|
case shouldCollapseRoomStateEvents
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||||
@@ -61,7 +62,7 @@ final class AppSettings: ObservableObject {
|
|||||||
/// The last known version of the app that was launched on this device, which is
|
/// The last known version of the app that was launched on this device, which is
|
||||||
/// used to detect when migrations should be run. When `nil` the app may have been
|
/// used to detect when migrations should be run. When `nil` the app may have been
|
||||||
/// deleted between runs so should clear data in the shared container and keychain.
|
/// deleted between runs so should clear data in the shared container and keychain.
|
||||||
@UserSetting(key: UserDefaultsKeys.lastVersionLaunched.rawValue, defaultValue: nil, storage: store)
|
@UserSetting(key: UserDefaultsKeys.lastVersionLaunched.rawValue, defaultValue: nil, persistIn: store)
|
||||||
var lastVersionLaunched: String?
|
var lastVersionLaunched: String?
|
||||||
|
|
||||||
/// The default homeserver address used. This is intentionally a string without a scheme
|
/// The default homeserver address used. This is intentionally a string without a scheme
|
||||||
@@ -123,27 +124,30 @@ final class AppSettings: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// `true` when the user has opted in to send analytics.
|
/// `true` when the user has opted in to send analytics.
|
||||||
@UserSetting(key: UserDefaultsKeys.enableAnalytics.rawValue, defaultValue: false, storage: store)
|
@UserSetting(key: UserDefaultsKeys.enableAnalytics.rawValue, defaultValue: false, persistIn: store)
|
||||||
var enableAnalytics
|
var enableAnalytics
|
||||||
|
|
||||||
/// Indicates if the device has already called identify for this session to PostHog.
|
/// Indicates if the device has already called identify for this session to PostHog.
|
||||||
/// This is separate to `enableAnalytics` as logging out leaves analytics
|
/// This is separate to `enableAnalytics` as logging out leaves analytics
|
||||||
/// enabled, but requires the next account to be identified separately.
|
/// enabled, but requires the next account to be identified separately.
|
||||||
@UserSetting(key: UserDefaultsKeys.isIdentifiedForAnalytics.rawValue, defaultValue: false, storage: store)
|
@UserSetting(key: UserDefaultsKeys.isIdentifiedForAnalytics.rawValue, defaultValue: false, persistIn: store)
|
||||||
var isIdentifiedForAnalytics
|
var isIdentifiedForAnalytics
|
||||||
|
|
||||||
// MARK: - Room Screen
|
// MARK: - Room Screen
|
||||||
|
|
||||||
@UserSettingRawRepresentable(key: UserDefaultsKeys.timelineStyle.rawValue, defaultValue: TimelineStyle.bubbles, storage: store)
|
@UserSettingRawRepresentable(key: UserDefaultsKeys.timelineStyle.rawValue, defaultValue: TimelineStyle.bubbles, persistIn: store)
|
||||||
var timelineStyle
|
var timelineStyle
|
||||||
|
|
||||||
|
@UserSetting(key: UserDefaultsKeys.shouldCollapseRoomStateEvents.rawValue, defaultValue: true, persistIn: nil)
|
||||||
|
var shouldCollapseRoomStateEvents
|
||||||
|
|
||||||
// MARK: - Notifications
|
// MARK: - Notifications
|
||||||
|
|
||||||
@UserSetting(key: UserDefaultsKeys.timelineStyle.rawValue, defaultValue: true, storage: store)
|
@UserSetting(key: UserDefaultsKeys.timelineStyle.rawValue, defaultValue: true, persistIn: store)
|
||||||
var enableInAppNotifications
|
var enableInAppNotifications
|
||||||
|
|
||||||
/// Tag describing which set of device specific rules a pusher executes.
|
/// Tag describing which set of device specific rules a pusher executes.
|
||||||
@UserSetting(key: UserDefaultsKeys.pusherProfileTag.rawValue, defaultValue: nil, storage: store)
|
@UserSetting(key: UserDefaultsKeys.pusherProfileTag.rawValue, defaultValue: nil, persistIn: store)
|
||||||
var pusherProfileTag: String?
|
var pusherProfileTag: String?
|
||||||
|
|
||||||
// MARK: - Other
|
// MARK: - Other
|
||||||
|
|||||||
@@ -120,6 +120,10 @@ extension ElementL10n {
|
|||||||
public static func roomTimelineReplyingTo(_ p1: Any) -> String {
|
public static func roomTimelineReplyingTo(_ p1: Any) -> String {
|
||||||
return ElementL10n.tr("Untranslated", "room_timeline_replying_to", String(describing: p1))
|
return ElementL10n.tr("Untranslated", "room_timeline_replying_to", String(describing: p1))
|
||||||
}
|
}
|
||||||
|
/// %d room changes
|
||||||
|
public static func roomTimelineStateChanges(_ p1: Int) -> String {
|
||||||
|
return ElementL10n.tr("Untranslated", "room_timeline_state_changes", p1)
|
||||||
|
}
|
||||||
/// Bubbles
|
/// Bubbles
|
||||||
public static let roomTimelineStyleBubbledLongDescription = ElementL10n.tr("Untranslated", "room_timeline_style_bubbled_long_description")
|
public static let roomTimelineStyleBubbledLongDescription = ElementL10n.tr("Untranslated", "room_timeline_style_bubbled_long_description")
|
||||||
/// Modern
|
/// Modern
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
extension Array {
|
||||||
|
func groupBy(_ isGroupable: (Element) -> Bool) -> [[Element]] {
|
||||||
|
var newItems = [[Element]]()
|
||||||
|
|
||||||
|
// Cache groupable states to avoid recomputing them later
|
||||||
|
let groupableStateByIndex = map(isGroupable)
|
||||||
|
|
||||||
|
var itemsToBeGrouped = [Element]()
|
||||||
|
for (index, currentItem) in enumerated() {
|
||||||
|
let previousItem = self[safe: index - 1]
|
||||||
|
let nextItem = self[safe: index + 1]
|
||||||
|
|
||||||
|
if groupableStateByIndex[safe: index] == true {
|
||||||
|
// At the begining of a groupable slice, very first message
|
||||||
|
if previousItem == nil, groupableStateByIndex[safe: index + 1] == true {
|
||||||
|
itemsToBeGrouped.append(currentItem)
|
||||||
|
}
|
||||||
|
// Still at the begining of a groupable slice
|
||||||
|
else if groupableStateByIndex[safe: index - 1] == false, groupableStateByIndex[safe: index + 1] == true {
|
||||||
|
itemsToBeGrouped.append(currentItem)
|
||||||
|
}
|
||||||
|
// In the middle of a groupable slice
|
||||||
|
else if !itemsToBeGrouped.isEmpty {
|
||||||
|
itemsToBeGrouped.append(currentItem)
|
||||||
|
}
|
||||||
|
// Solitary groupable item
|
||||||
|
else {
|
||||||
|
newItems.append([currentItem])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last item in the list. Try finishing the groupable slice
|
||||||
|
if nextItem == nil, !itemsToBeGrouped.isEmpty {
|
||||||
|
newItems.append(itemsToBeGrouped)
|
||||||
|
itemsToBeGrouped.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Finished a groupable slice
|
||||||
|
if !itemsToBeGrouped.isEmpty {
|
||||||
|
newItems.append(itemsToBeGrouped)
|
||||||
|
itemsToBeGrouped.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the current element too
|
||||||
|
newItems.append([currentItem])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newItems
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,25 +26,31 @@ import Foundation
|
|||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
struct UserSetting<Value: Equatable> {
|
struct UserSetting<Value: Equatable> {
|
||||||
private let key: String
|
private let key: String
|
||||||
private let defaultValue: Value
|
private var defaultValue: Value
|
||||||
private let storage: UserDefaults
|
private let storage: UserDefaults?
|
||||||
private let publisher: CurrentValueSubject<Value, Never>
|
private let publisher: CurrentValueSubject<Value, Never>
|
||||||
|
|
||||||
init(key: String, defaultValue: Value, storage: UserDefaults = .standard) {
|
init(key: String, defaultValue: Value, persistIn storage: UserDefaults?) {
|
||||||
self.key = key
|
self.key = key
|
||||||
self.defaultValue = defaultValue
|
self.defaultValue = defaultValue
|
||||||
self.storage = storage
|
self.storage = storage
|
||||||
|
|
||||||
let value = storage.value(forKey: key) as? Value ?? defaultValue
|
let value = storage?.value(forKey: key) as? Value ?? defaultValue
|
||||||
publisher = CurrentValueSubject<Value, Never>(value)
|
publisher = CurrentValueSubject<Value, Never>(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var wrappedValue: Value {
|
var wrappedValue: Value {
|
||||||
get {
|
get {
|
||||||
let value = storage.value(forKey: key) as? Value
|
let value = storage?.value(forKey: key) as? Value
|
||||||
return value ?? defaultValue
|
return value ?? defaultValue
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
|
guard let storage else {
|
||||||
|
defaultValue = newValue
|
||||||
|
publisher.send(defaultValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if let optional = newValue as? AnyOptional, optional.isNil {
|
if let optional = newValue as? AnyOptional, optional.isNil {
|
||||||
storage.removeObject(forKey: key)
|
storage.removeObject(forKey: key)
|
||||||
publisher.send(defaultValue)
|
publisher.send(defaultValue)
|
||||||
@@ -61,8 +67,8 @@ struct UserSetting<Value: Equatable> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension UserSetting where Value: ExpressibleByNilLiteral {
|
extension UserSetting where Value: ExpressibleByNilLiteral {
|
||||||
init(key: String, storage: UserDefaults = .standard) {
|
init(key: String, persistIn storage: UserDefaults?) {
|
||||||
self.init(key: key, defaultValue: nil, storage: storage)
|
self.init(key: key, defaultValue: nil, persistIn: storage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,28 +80,34 @@ extension UserSetting where Value: ExpressibleByNilLiteral {
|
|||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
struct UserSettingRawRepresentable<Value: RawRepresentable & Equatable> {
|
struct UserSettingRawRepresentable<Value: RawRepresentable & Equatable> {
|
||||||
private let key: String
|
private let key: String
|
||||||
private let defaultValue: Value
|
private var defaultValue: Value
|
||||||
private let storage: UserDefaults
|
private let storage: UserDefaults?
|
||||||
private let publisher: CurrentValueSubject<Value, Never>
|
private let publisher: CurrentValueSubject<Value, Never>
|
||||||
|
|
||||||
init(key: String, defaultValue: Value, storage: UserDefaults = .standard) {
|
init(key: String, defaultValue: Value, persistIn storage: UserDefaults?) {
|
||||||
self.key = key
|
self.key = key
|
||||||
self.defaultValue = defaultValue
|
self.defaultValue = defaultValue
|
||||||
self.storage = storage
|
self.storage = storage
|
||||||
|
|
||||||
let value = (storage.value(forKey: key) as? Value.RawValue).flatMap { Value(rawValue: $0) } ?? defaultValue
|
let value = (storage?.value(forKey: key) as? Value.RawValue).flatMap { Value(rawValue: $0) } ?? defaultValue
|
||||||
publisher = CurrentValueSubject<Value, Never>(value)
|
publisher = CurrentValueSubject<Value, Never>(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var wrappedValue: Value {
|
var wrappedValue: Value {
|
||||||
get {
|
get {
|
||||||
guard let value = storage.value(forKey: key) as? Value.RawValue else {
|
guard let value = storage?.value(forKey: key) as? Value.RawValue else {
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
return Value(rawValue: value) ?? defaultValue
|
return Value(rawValue: value) ?? defaultValue
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
|
guard let storage else {
|
||||||
|
defaultValue = newValue
|
||||||
|
publisher.send(defaultValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if let optional = newValue as? AnyOptional, optional.isNil {
|
if let optional = newValue as? AnyOptional, optional.isNil {
|
||||||
storage.removeObject(forKey: key)
|
storage.removeObject(forKey: key)
|
||||||
publisher.send(newValue)
|
publisher.send(newValue)
|
||||||
@@ -112,8 +124,8 @@ struct UserSettingRawRepresentable<Value: RawRepresentable & Equatable> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension UserSettingRawRepresentable where Value: ExpressibleByNilLiteral {
|
extension UserSettingRawRepresentable where Value: ExpressibleByNilLiteral {
|
||||||
init(key: String, storage: UserDefaults = .standard) {
|
init(key: String, persistIn storage: UserDefaults?) {
|
||||||
self.init(key: key, defaultValue: nil, storage: storage)
|
self.init(key: key, defaultValue: nil, persistIn: storage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ struct DeveloperOptionsScreenViewState: BindableState {
|
|||||||
var bindings: DeveloperOptionsScreenViewStateBindings
|
var bindings: DeveloperOptionsScreenViewStateBindings
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DeveloperOptionsScreenViewStateBindings { }
|
struct DeveloperOptionsScreenViewStateBindings {
|
||||||
|
var shouldCollapseRoomStateEvents: Bool
|
||||||
|
}
|
||||||
|
|
||||||
enum DeveloperOptionsScreenViewAction { }
|
enum DeveloperOptionsScreenViewAction {
|
||||||
|
case changedShouldCollapseRoomStateEvents
|
||||||
|
}
|
||||||
|
|||||||
+13
-2
@@ -20,8 +20,19 @@ typealias DeveloperOptionsScreenViewModelType = StateStoreViewModel<DeveloperOpt
|
|||||||
|
|
||||||
class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, DeveloperOptionsScreenViewModelProtocol {
|
class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, DeveloperOptionsScreenViewModelProtocol {
|
||||||
var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)?
|
var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
super.init(initialViewState: DeveloperOptionsScreenViewState(bindings: DeveloperOptionsScreenViewStateBindings()))
|
super.init(initialViewState: DeveloperOptionsScreenViewState(bindings: DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: ServiceLocator.shared.settings.shouldCollapseRoomStateEvents)))
|
||||||
|
|
||||||
|
ServiceLocator.shared.settings.$shouldCollapseRoomStateEvents
|
||||||
|
.weakAssign(to: \.state.bindings.shouldCollapseRoomStateEvents, on: self)
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func process(viewAction: DeveloperOptionsScreenViewAction) async {
|
||||||
|
switch viewAction {
|
||||||
|
case .changedShouldCollapseRoomStateEvents:
|
||||||
|
ServiceLocator.shared.settings.shouldCollapseRoomStateEvents = state.bindings.shouldCollapseRoomStateEvents
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -22,6 +22,15 @@ struct DeveloperOptionsScreenScreen: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
Section {
|
||||||
|
Toggle(isOn: $context.shouldCollapseRoomStateEvents) {
|
||||||
|
Text("Collapse room state events")
|
||||||
|
}
|
||||||
|
.onChange(of: context.shouldCollapseRoomStateEvents) { _ in
|
||||||
|
context.send(viewAction: .changedShouldCollapseRoomStateEvents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
showConfetti = true
|
showConfetti = true
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
viewModel = RoomScreenViewModel(timelineController: parameters.timelineController,
|
viewModel = RoomScreenViewModel(timelineController: parameters.timelineController,
|
||||||
timelineViewFactory: RoomTimelineViewFactory(),
|
|
||||||
mediaProvider: parameters.mediaProvider,
|
mediaProvider: parameters.mediaProvider,
|
||||||
roomName: parameters.roomProxy.displayName ?? parameters.roomProxy.name,
|
roomName: parameters.roomProxy.displayName ?? parameters.roomProxy.name,
|
||||||
roomAvatarUrl: parameters.roomProxy.avatarURL)
|
roomAvatarUrl: parameters.roomProxy.avatarURL)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Algorithms
|
||||||
import Combine
|
import Combine
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@@ -27,15 +28,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let timelineController: RoomTimelineControllerProtocol
|
private let timelineController: RoomTimelineControllerProtocol
|
||||||
private let timelineViewFactory: RoomTimelineViewFactoryProtocol
|
|
||||||
|
|
||||||
init(timelineController: RoomTimelineControllerProtocol,
|
init(timelineController: RoomTimelineControllerProtocol,
|
||||||
timelineViewFactory: RoomTimelineViewFactoryProtocol,
|
|
||||||
mediaProvider: MediaProviderProtocol,
|
mediaProvider: MediaProviderProtocol,
|
||||||
roomName: String?,
|
roomName: String?,
|
||||||
roomAvatarUrl: URL? = nil) {
|
roomAvatarUrl: URL? = nil) {
|
||||||
self.timelineController = timelineController
|
self.timelineController = timelineController
|
||||||
self.timelineViewFactory = timelineViewFactory
|
|
||||||
|
|
||||||
super.init(initialViewState: RoomScreenViewState(roomId: timelineController.roomID,
|
super.init(initialViewState: RoomScreenViewState(roomId: timelineController.roomID,
|
||||||
roomTitle: roomName ?? "Unknown room 💥",
|
roomTitle: roomName ?? "Unknown room 💥",
|
||||||
@@ -52,13 +50,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
switch callback {
|
switch callback {
|
||||||
case .updatedTimelineItems:
|
case .updatedTimelineItems:
|
||||||
self.buildTimelineViews()
|
self.buildTimelineViews()
|
||||||
case .updatedTimelineItem(let itemId):
|
|
||||||
guard let timelineItem = self.timelineController.timelineItems.first(where: { $0.id == itemId }),
|
|
||||||
let viewIndex = self.state.items.firstIndex(where: { $0.id == itemId }) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.state.items[viewIndex] = timelineViewFactory.buildTimelineViewFor(timelineItem: timelineItem)
|
|
||||||
case .canBackPaginate(let canBackPaginate):
|
case .canBackPaginate(let canBackPaginate):
|
||||||
if self.state.canBackPaginate != canBackPaginate {
|
if self.state.canBackPaginate != canBackPaginate {
|
||||||
self.state.canBackPaginate = canBackPaginate
|
self.state.canBackPaginate = canBackPaginate
|
||||||
@@ -161,13 +152,57 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func buildTimelineViews() {
|
private func buildTimelineViews() {
|
||||||
let stateItems = timelineController.timelineItems.map { item in
|
var timelineViews = [RoomTimelineViewProvider]()
|
||||||
timelineViewFactory.buildTimelineViewFor(timelineItem: item)
|
|
||||||
|
let itemsGroupedByTimelineDisplayStyle = timelineController.timelineItems.chunked { current, next in
|
||||||
|
canGroupItem(timelineItem: current, with: next)
|
||||||
}
|
}
|
||||||
|
|
||||||
state.items = stateItems
|
for itemGroup in itemsGroupedByTimelineDisplayStyle {
|
||||||
|
guard !itemGroup.isEmpty else {
|
||||||
|
MXLog.error("Found empty item group")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemGroup.count == 1 {
|
||||||
|
if let firstItem = itemGroup.first {
|
||||||
|
timelineViews.append(RoomTimelineViewProvider(timelineItem: firstItem, grouping: .single))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (index, item) in itemGroup.enumerated() {
|
||||||
|
if index == 0 {
|
||||||
|
timelineViews.append(RoomTimelineViewProvider(timelineItem: item, grouping: .first))
|
||||||
|
} else if index == itemGroup.count - 1 {
|
||||||
|
timelineViews.append(RoomTimelineViewProvider(timelineItem: item, grouping: .last))
|
||||||
|
} else {
|
||||||
|
timelineViews.append(RoomTimelineViewProvider(timelineItem: item, grouping: .middle))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.items = timelineViews
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func canGroupItem(timelineItem: RoomTimelineItemProtocol, with otherTimelineItem: RoomTimelineItemProtocol) -> Bool {
|
||||||
|
if timelineItem is CollapsibleTimelineItem || otherTimelineItem is CollapsibleTimelineItem {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol,
|
||||||
|
let otherEventTimelineItem = otherTimelineItem as? EventBasedTimelineItemProtocol else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// State events aren't rendered as messages so shouldn't be grouped.
|
||||||
|
if eventTimelineItem is StateRoomTimelineItem || otherEventTimelineItem is StateRoomTimelineItem {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// can be improved by adding a date threshold
|
||||||
|
return otherEventTimelineItem.properties.reactions.isEmpty && eventTimelineItem.sender == otherEventTimelineItem.sender
|
||||||
|
}
|
||||||
|
|
||||||
private func sendCurrentMessage() async {
|
private func sendCurrentMessage() async {
|
||||||
guard !state.bindings.composerText.isEmpty else {
|
guard !state.bindings.composerText.isEmpty else {
|
||||||
fatalError("This message should never be empty")
|
fatalError("This message should never be empty")
|
||||||
@@ -298,7 +333,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
|
|
||||||
extension RoomScreenViewModel {
|
extension RoomScreenViewModel {
|
||||||
static let mock = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
static let mock = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||||
timelineViewFactory: RoomTimelineViewFactory(),
|
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
roomName: "Preview room")
|
roomName: "Preview room")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ struct RoomHeaderView_Previews: PreviewProvider {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
static var bodyPlain: some View {
|
static var bodyPlain: some View {
|
||||||
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||||
timelineViewFactory: RoomTimelineViewFactory(),
|
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
roomName: "Some Room name",
|
roomName: "Some Room name",
|
||||||
roomAvatarUrl: URL.picturesDirectory)
|
roomAvatarUrl: URL.picturesDirectory)
|
||||||
@@ -67,11 +66,10 @@ struct RoomHeaderView_Previews: PreviewProvider {
|
|||||||
.previewLayout(.sizeThatFits)
|
.previewLayout(.sizeThatFits)
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
static var bodyEncrypted: some View {
|
static var bodyEncrypted: some View {
|
||||||
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||||
timelineViewFactory: RoomTimelineViewFactory(),
|
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
roomName: "Some Room name",
|
roomName: "Some Room name",
|
||||||
roomAvatarUrl: nil)
|
roomAvatarUrl: nil)
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ struct RoomScreen: View {
|
|||||||
struct RoomScreen_Previews: PreviewProvider {
|
struct RoomScreen_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||||
timelineViewFactory: RoomTimelineViewFactory(),
|
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
roomName: "Preview room")
|
roomName: "Preview room")
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ import SwiftUI
|
|||||||
|
|
||||||
struct TimelineItemBubbledStylerView<Content: View>: View {
|
struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||||
|
@Environment(\.timelineGroupStyle) private var timelineStyleGrouping
|
||||||
|
|
||||||
let timelineItem: EventBasedTimelineItemProtocol
|
let timelineItem: EventBasedTimelineItemProtocol
|
||||||
@ViewBuilder let content: () -> Content
|
@ViewBuilder let content: () -> Content
|
||||||
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
|
||||||
@ScaledMetric private var senderNameVerticalPadding = 3
|
@ScaledMetric private var senderNameVerticalPadding = 3
|
||||||
private let cornerRadius: CGFloat = 12
|
private let cornerRadius: CGFloat = 12
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var header: some View {
|
private var header: some View {
|
||||||
if timelineItem.shouldShowSenderDetails {
|
if shouldShowSenderDetails {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 8)
|
.frame(height: 8)
|
||||||
@@ -90,7 +90,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
|
|
||||||
var messageBubble: some View {
|
var messageBubble: some View {
|
||||||
styledContent
|
styledContent
|
||||||
.contentShape(.contextMenuPreview, RoundedCornerShape(radius: cornerRadius, corners: timelineItem.roundedCorners)) // Rounded corners for the context menu animation.
|
.contentShape(.contextMenuPreview, RoundedCornerShape(radius: cornerRadius, corners: roundedCorners)) // Rounded corners for the context menu animation.
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
context.viewState.contextMenuActionProvider?(timelineItem.id).map { actions in
|
context.viewState.contextMenuActionProvider?(timelineItem.id).map { actions in
|
||||||
TimelineItemContextMenu(itemID: timelineItem.id, contextMenuActions: actions)
|
TimelineItemContextMenu(itemID: timelineItem.id, contextMenuActions: actions)
|
||||||
@@ -105,7 +105,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
content()
|
content()
|
||||||
.bubbleStyle(inset: false,
|
.bubbleStyle(inset: false,
|
||||||
cornerRadius: cornerRadius,
|
cornerRadius: cornerRadius,
|
||||||
corners: timelineItem.roundedCorners)
|
corners: roundedCorners)
|
||||||
} else {
|
} else {
|
||||||
VStack(alignment: .trailing, spacing: 4) {
|
VStack(alignment: .trailing, spacing: 4) {
|
||||||
content()
|
content()
|
||||||
@@ -119,13 +119,13 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
.bubbleStyle(inset: true,
|
.bubbleStyle(inset: true,
|
||||||
color: timelineItem.isOutgoing ? .element.bubblesYou : .element.bubblesNotYou,
|
color: timelineItem.isOutgoing ? .element.bubblesYou : .element.bubblesNotYou,
|
||||||
cornerRadius: cornerRadius,
|
cornerRadius: cornerRadius,
|
||||||
corners: timelineItem.roundedCorners)
|
corners: roundedCorners)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var messageBubbleTopPadding: CGFloat {
|
private var messageBubbleTopPadding: CGFloat {
|
||||||
guard timelineItem.isOutgoing else { return 0 }
|
guard timelineItem.isOutgoing else { return 0 }
|
||||||
return timelineItem.groupState == .single || timelineItem.groupState == .beginning ? 8 : 0
|
return timelineStyleGrouping == .single || timelineStyleGrouping == .first ? 8 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private var shouldAvoidBubbling: Bool {
|
private var shouldAvoidBubbling: Bool {
|
||||||
@@ -135,6 +135,31 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
private var alignment: HorizontalAlignment {
|
private var alignment: HorizontalAlignment {
|
||||||
timelineItem.isOutgoing ? .trailing : .leading
|
timelineItem.isOutgoing ? .trailing : .leading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var roundedCorners: UIRectCorner {
|
||||||
|
switch timelineStyleGrouping {
|
||||||
|
case .single:
|
||||||
|
return .allCorners
|
||||||
|
case .first:
|
||||||
|
if timelineItem.isOutgoing {
|
||||||
|
return [.topLeft, .topRight, .bottomLeft]
|
||||||
|
} else {
|
||||||
|
return [.topLeft, .topRight, .bottomRight]
|
||||||
|
}
|
||||||
|
case .middle:
|
||||||
|
return timelineItem.isOutgoing ? [.topLeft, .bottomLeft] : [.topRight, .bottomRight]
|
||||||
|
case .last:
|
||||||
|
if timelineItem.isOutgoing {
|
||||||
|
return [.topLeft, .bottomLeft, .bottomRight]
|
||||||
|
} else {
|
||||||
|
return [.topRight, .bottomLeft, .bottomRight]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var shouldShowSenderDetails: Bool {
|
||||||
|
timelineStyleGrouping.shouldShowSenderDetails
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension View {
|
private extension View {
|
||||||
@@ -152,7 +177,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider {
|
|||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
ForEach(1..<MockRoomTimelineController().timelineItems.count, id: \.self) { index in
|
ForEach(1..<MockRoomTimelineController().timelineItems.count, id: \.self) { index in
|
||||||
let item = MockRoomTimelineController().timelineItems[index]
|
let item = MockRoomTimelineController().timelineItems[index]
|
||||||
RoomTimelineViewFactory().buildTimelineViewFor(timelineItem: item)
|
RoomTimelineViewProvider(timelineItem: item, grouping: .single)
|
||||||
.padding(TimelineStyle.bubbles.rowInsets) // Insets added in the table view cells
|
.padding(TimelineStyle.bubbles.rowInsets) // Insets added in the table view cells
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct TimelineItemPlainStylerView<Content: View>: View {
|
struct TimelineItemPlainStylerView<Content: View>: View {
|
||||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||||
|
@Environment(\.timelineGroupStyle) private var timelineStyleGrouping
|
||||||
|
|
||||||
let timelineItem: EventBasedTimelineItemProtocol
|
let timelineItem: EventBasedTimelineItemProtocol
|
||||||
@ViewBuilder let content: () -> Content
|
@ViewBuilder let content: () -> Content
|
||||||
@@ -44,7 +45,7 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var header: some View {
|
private var header: some View {
|
||||||
if timelineItem.shouldShowSenderDetails {
|
if shouldShowSenderDetails {
|
||||||
HStack {
|
HStack {
|
||||||
TimelineSenderAvatarView(timelineItem: timelineItem)
|
TimelineSenderAvatarView(timelineItem: timelineItem)
|
||||||
Text(timelineItem.sender.displayName ?? timelineItem.sender.id)
|
Text(timelineItem.sender.displayName ?? timelineItem.sender.id)
|
||||||
@@ -78,6 +79,10 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var shouldShowSenderDetails: Bool {
|
||||||
|
timelineStyleGrouping.shouldShowSenderDetails
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TimelineItemPlainStylerView_Previews: PreviewProvider {
|
struct TimelineItemPlainStylerView_Previews: PreviewProvider {
|
||||||
@@ -87,7 +92,7 @@ struct TimelineItemPlainStylerView_Previews: PreviewProvider {
|
|||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
ForEach(1..<MockRoomTimelineController().timelineItems.count, id: \.self) { index in
|
ForEach(1..<MockRoomTimelineController().timelineItems.count, id: \.self) { index in
|
||||||
let item = MockRoomTimelineController().timelineItems[index]
|
let item = MockRoomTimelineController().timelineItems[index]
|
||||||
RoomTimelineViewFactory().buildTimelineViewFor(timelineItem: item)
|
RoomTimelineViewProvider(timelineItem: item, grouping: .single)
|
||||||
.padding(TimelineStyle.plain.rowInsets) // Insets added in the table view cells
|
.padding(TimelineStyle.plain.rowInsets) // Insets added in the table view cells
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,21 +32,50 @@ enum TimelineStyle: String, CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TimelineGroupStyle: Hashable {
|
||||||
|
case single
|
||||||
|
case first
|
||||||
|
case middle
|
||||||
|
case last
|
||||||
|
|
||||||
|
var shouldShowSenderDetails: Bool {
|
||||||
|
switch self {
|
||||||
|
case .single, .first:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Environment
|
// MARK: - Environment
|
||||||
|
|
||||||
private struct TimelineStyleKey: EnvironmentKey {
|
private struct TimelineStyleKey: EnvironmentKey {
|
||||||
static let defaultValue = TimelineStyle.bubbles
|
static let defaultValue = TimelineStyle.bubbles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct TimelineGroupStyleKey: EnvironmentKey {
|
||||||
|
static let defaultValue = TimelineGroupStyle.single
|
||||||
|
}
|
||||||
|
|
||||||
extension EnvironmentValues {
|
extension EnvironmentValues {
|
||||||
var timelineStyle: TimelineStyle {
|
var timelineStyle: TimelineStyle {
|
||||||
get { self[TimelineStyleKey.self] }
|
get { self[TimelineStyleKey.self] }
|
||||||
set { self[TimelineStyleKey.self] = newValue }
|
set { self[TimelineStyleKey.self] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var timelineGroupStyle: TimelineGroupStyle {
|
||||||
|
get { self[TimelineGroupStyleKey.self] }
|
||||||
|
set { self[TimelineGroupStyleKey.self] = newValue }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
func timelineStyle(_ style: TimelineStyle) -> some View {
|
func timelineStyle(_ style: TimelineStyle) -> some View {
|
||||||
environment(\.timelineStyle, style)
|
environment(\.timelineStyle, style)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func timelineStyleGrouping(_ grouping: TimelineGroupStyle) -> some View {
|
||||||
|
environment(\.timelineGroupStyle, grouping)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ struct AudioRoomTimelineView_Previews: PreviewProvider {
|
|||||||
AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: UUID().uuidString,
|
AudioRoomTimelineView(timelineItem: AudioRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "audio.ogg",
|
body: "audio.ogg",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
//
|
||||||
|
// 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 SwiftUI
|
||||||
|
|
||||||
|
struct CollapsibleRoomTimelineView: View {
|
||||||
|
private let timelineItem: CollapsibleTimelineItem
|
||||||
|
private let timelineViews: [RoomTimelineViewProvider]
|
||||||
|
|
||||||
|
@State private var isExpanded = false
|
||||||
|
|
||||||
|
init(timelineItem: CollapsibleTimelineItem) {
|
||||||
|
self.timelineItem = timelineItem
|
||||||
|
timelineViews = timelineItem.items.map { RoomTimelineViewProvider(timelineItem: $0, grouping: .single) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
DisclosureGroup(ElementL10n.roomTimelineStateChanges(timelineItem.items.count),
|
||||||
|
isExpanded: $isExpanded) {
|
||||||
|
Group {
|
||||||
|
ForEach(timelineViews) { timelineView in
|
||||||
|
timelineView.body
|
||||||
|
}
|
||||||
|
}.transition(.opacity.animation(.elementDefault))
|
||||||
|
}
|
||||||
|
.disclosureGroupStyle(CollapsibleRoomTimelineItemDisclosureGroupStyle())
|
||||||
|
.transaction { transaction in
|
||||||
|
transaction.animation = .noAnimation // Fixes weird animations on the disclosure indicator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CollapsibleRoomTimelineView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let item = CollapsibleTimelineItem(items: [SeparatorRoomTimelineItem(text: "This is a separator"),
|
||||||
|
SeparatorRoomTimelineItem(text: "This is another separator")])
|
||||||
|
CollapsibleRoomTimelineView(timelineItem: item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct CollapsibleRoomTimelineItemDisclosureGroupStyle: DisclosureGroupStyle {
|
||||||
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
VStack {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
configuration.label
|
||||||
|
Text(Image(systemName: "chevron.forward"))
|
||||||
|
.rotationEffect(.degrees(configuration.isExpanded ? 90 : 0))
|
||||||
|
.animation(.elementDefault, value: configuration.isExpanded)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.font(.element.footnote)
|
||||||
|
.foregroundColor(.element.secondaryContent)
|
||||||
|
.padding(.horizontal, 36)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.onTapGesture {
|
||||||
|
configuration.isExpanded.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
if configuration.isExpanded {
|
||||||
|
configuration.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,7 +55,6 @@ struct EmoteRoomTimelineView_Previews: PreviewProvider {
|
|||||||
EmoteRoomTimelineItem(id: UUID().uuidString,
|
EmoteRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: text,
|
body: text,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: senderId))
|
sender: .init(id: senderId))
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ struct EncryptedRoomTimelineView_Previews: PreviewProvider {
|
|||||||
body: text,
|
body: text,
|
||||||
encryptionType: .unknown,
|
encryptionType: .unknown,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: senderId))
|
sender: .init(id: senderId))
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
|
|||||||
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "document.pdf",
|
body: "document.pdf",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
@@ -56,7 +55,6 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
|
|||||||
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "document.docx",
|
body: "document.docx",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
@@ -66,7 +64,6 @@ struct FileRoomTimelineView_Previews: PreviewProvider {
|
|||||||
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "document.txt",
|
body: "document.txt",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
|||||||
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "Some image",
|
body: "Some image",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
@@ -83,7 +82,6 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
|||||||
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "Some other image",
|
body: "Some other image",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
@@ -92,7 +90,6 @@ struct ImageRoomTimelineView_Previews: PreviewProvider {
|
|||||||
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
ImageRoomTimelineView(timelineItem: ImageRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "Blurhashed image",
|
body: "Blurhashed image",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ struct NoticeRoomTimelineView_Previews: PreviewProvider {
|
|||||||
NoticeRoomTimelineItem(id: UUID().uuidString,
|
NoticeRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: text,
|
body: text,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: senderId))
|
sender: .init(id: senderId))
|
||||||
|
|||||||
@@ -41,25 +41,23 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider {
|
|||||||
static let item = ReadMarkerRoomTimelineItem()
|
static let item = ReadMarkerRoomTimelineItem()
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
RoomTimelineViewProvider.separator(.init(text: "Today"))
|
RoomTimelineViewProvider.separator(.init(text: "Today"), .single)
|
||||||
RoomTimelineViewProvider.text(.init(id: "",
|
RoomTimelineViewProvider.text(.init(id: "",
|
||||||
body: "This is another message",
|
body: "This is another message",
|
||||||
timestamp: "",
|
timestamp: "",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "1", displayName: "Bob")))
|
sender: .init(id: "1", displayName: "Bob")), .single)
|
||||||
|
|
||||||
ReadMarkerRoomTimelineView(timelineItem: item)
|
ReadMarkerRoomTimelineView(timelineItem: item)
|
||||||
|
|
||||||
RoomTimelineViewProvider.separator(.init(text: "Today"))
|
RoomTimelineViewProvider.separator(.init(text: "Today"), .single)
|
||||||
RoomTimelineViewProvider.text(.init(id: "",
|
RoomTimelineViewProvider.text(.init(id: "",
|
||||||
body: "This is a message",
|
body: "This is a message",
|
||||||
timestamp: "",
|
timestamp: "",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "", displayName: "Alice")))
|
sender: .init(id: "", displayName: "Alice")), .single)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 8)
|
.padding(.horizontal, 8)
|
||||||
.environmentObject(viewModel.context)
|
.environmentObject(viewModel.context)
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ struct RedactedRoomTimelineView_Previews: PreviewProvider {
|
|||||||
RedactedRoomTimelineItem(id: UUID().uuidString,
|
RedactedRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: text,
|
body: text,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: senderId))
|
sender: .init(id: senderId))
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ struct StateRoomTimelineView_Previews: PreviewProvider {
|
|||||||
static let item = StateRoomTimelineItem(id: UUID().uuidString,
|
static let item = StateRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "Alice joined",
|
body: "Alice joined",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .beginning,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: ""))
|
sender: .init(id: ""))
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
|||||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "Some image",
|
body: "Some image",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
@@ -68,7 +67,6 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
|||||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "Some other image",
|
body: "Some other image",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
@@ -77,7 +75,6 @@ struct StickerRoomTimelineView_Previews: PreviewProvider {
|
|||||||
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
StickerRoomTimelineView(timelineItem: StickerRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "Blurhashed image",
|
body: "Blurhashed image",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ struct TextRoomTimelineView_Previews: PreviewProvider {
|
|||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: text,
|
body: text,
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: isOutgoing,
|
isEditable: isOutgoing,
|
||||||
sender: .init(id: senderId))
|
sender: .init(id: senderId))
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ struct UnsupportedRoomTimelineView_Previews: PreviewProvider {
|
|||||||
eventType: "Some Event Type",
|
eventType: "Some Event Type",
|
||||||
error: "Something went wrong",
|
error: "Something went wrong",
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: senderId))
|
sender: .init(id: senderId))
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
|||||||
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "Some video",
|
body: "Some video",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
@@ -80,7 +79,6 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
|||||||
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "Some other video",
|
body: "Some other video",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
@@ -91,7 +89,6 @@ struct VideoRoomTimelineView_Previews: PreviewProvider {
|
|||||||
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
VideoRoomTimelineView(timelineItem: VideoRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "Blurhashed video",
|
body: "Blurhashed video",
|
||||||
timestamp: "Now",
|
timestamp: "Now",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ struct TimelineView: UIViewControllerRepresentable {
|
|||||||
|
|
||||||
struct TimelineTableView_Previews: PreviewProvider {
|
struct TimelineTableView_Previews: PreviewProvider {
|
||||||
static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
static let viewModel = RoomScreenViewModel(timelineController: MockRoomTimelineController(),
|
||||||
timelineViewFactory: RoomTimelineViewFactory(),
|
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
roomName: "Preview room")
|
roomName: "Preview room")
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ enum RoomTimelineItemFixtures {
|
|||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "That looks so good!",
|
body: "That looks so good!",
|
||||||
timestamp: "10:10 AM",
|
timestamp: "10:10 AM",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "", displayName: "Jacob"),
|
sender: .init(id: "", displayName: "Jacob"),
|
||||||
@@ -31,7 +30,6 @@ enum RoomTimelineItemFixtures {
|
|||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "Let’s get lunch soon! New salad place opened up 🥗. When are y’all free? 🤗",
|
body: "Let’s get lunch soon! New salad place opened up 🥗. When are y’all free? 🤗",
|
||||||
timestamp: "10:11 AM",
|
timestamp: "10:11 AM",
|
||||||
groupState: .beginning,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "", displayName: "Helena"),
|
sender: .init(id: "", displayName: "Helena"),
|
||||||
@@ -41,7 +39,6 @@ enum RoomTimelineItemFixtures {
|
|||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "I can be around on Wednesday. How about some 🌮 instead? Like https://www.tortilla.co.uk/",
|
body: "I can be around on Wednesday. How about some 🌮 instead? Like https://www.tortilla.co.uk/",
|
||||||
timestamp: "10:11 AM",
|
timestamp: "10:11 AM",
|
||||||
groupState: .end,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "", displayName: "Helena"),
|
sender: .init(id: "", displayName: "Helena"),
|
||||||
@@ -53,21 +50,18 @@ enum RoomTimelineItemFixtures {
|
|||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
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!",
|
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",
|
timestamp: "5 PM",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "", displayName: "Helena")),
|
sender: .init(id: "", displayName: "Helena")),
|
||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "And John's speech was amazing!",
|
body: "And John's speech was amazing!",
|
||||||
timestamp: "5 PM",
|
timestamp: "5 PM",
|
||||||
groupState: .beginning,
|
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
isEditable: true,
|
isEditable: true,
|
||||||
sender: .init(id: "", displayName: "Bob")),
|
sender: .init(id: "", displayName: "Bob")),
|
||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
body: "New home office set up!",
|
body: "New home office set up!",
|
||||||
timestamp: "5 PM",
|
timestamp: "5 PM",
|
||||||
groupState: .end,
|
|
||||||
isOutgoing: true,
|
isOutgoing: true,
|
||||||
isEditable: true,
|
isEditable: true,
|
||||||
sender: .init(id: "", displayName: "Bob"),
|
sender: .init(id: "", displayName: "Bob"),
|
||||||
@@ -79,7 +73,6 @@ enum RoomTimelineItemFixtures {
|
|||||||
body: "",
|
body: "",
|
||||||
formattedBody: AttributedStringBuilder().fromHTML("Hol' up <blockquote>New home office set up!</blockquote>That's amazing! Congrats 🥳"),
|
formattedBody: AttributedStringBuilder().fromHTML("Hol' up <blockquote>New home office set up!</blockquote>That's amazing! Congrats 🥳"),
|
||||||
timestamp: "5 PM",
|
timestamp: "5 PM",
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: false,
|
isOutgoing: false,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "", displayName: "Helena"))
|
sender: .init(id: "", displayName: "Helena"))
|
||||||
@@ -88,24 +81,20 @@ enum RoomTimelineItemFixtures {
|
|||||||
/// A small chunk of events, containing 2 text items.
|
/// A small chunk of events, containing 2 text items.
|
||||||
static var smallChunk: [RoomTimelineItemProtocol] {
|
static var smallChunk: [RoomTimelineItemProtocol] {
|
||||||
[TextRoomTimelineItem(text: "Hey there 👋",
|
[TextRoomTimelineItem(text: "Hey there 👋",
|
||||||
groupState: .beginning,
|
|
||||||
senderDisplayName: "Alice"),
|
senderDisplayName: "Alice"),
|
||||||
TextRoomTimelineItem(text: "How are you?",
|
TextRoomTimelineItem(text: "How are you?",
|
||||||
groupState: .end,
|
|
||||||
senderDisplayName: "Alice")]
|
senderDisplayName: "Alice")]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A chunk of events that contains a single text item.
|
/// A chunk of events that contains a single text item.
|
||||||
static var singleMessageChunk: [RoomTimelineItemProtocol] {
|
static var singleMessageChunk: [RoomTimelineItemProtocol] {
|
||||||
[TextRoomTimelineItem(text: "Tap tap tap 🎙️. Is this thing on?",
|
[TextRoomTimelineItem(text: "Tap tap tap 🎙️. Is this thing on?",
|
||||||
groupState: .single,
|
|
||||||
senderDisplayName: "Helena")]
|
senderDisplayName: "Helena")]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A single text item.
|
/// A single text item.
|
||||||
static var incomingMessage: RoomTimelineItemProtocol {
|
static var incomingMessage: RoomTimelineItemProtocol {
|
||||||
TextRoomTimelineItem(text: "Hello, World!",
|
TextRoomTimelineItem(text: "Hello, World!",
|
||||||
groupState: .single,
|
|
||||||
senderDisplayName: "Bob")
|
senderDisplayName: "Bob")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,11 +185,10 @@ enum RoomTimelineItemFixtures {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extension TextRoomTimelineItem {
|
private extension TextRoomTimelineItem {
|
||||||
init(text: String, groupState: TimelineItemGroupState = .single, senderDisplayName: String) {
|
init(text: String, senderDisplayName: String) {
|
||||||
self.init(id: UUID().uuidString,
|
self.init(id: UUID().uuidString,
|
||||||
body: text,
|
body: text,
|
||||||
timestamp: "10:47 am",
|
timestamp: "10:47 am",
|
||||||
groupState: groupState,
|
|
||||||
isOutgoing: senderDisplayName == "Alice",
|
isOutgoing: senderDisplayName == "Alice",
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: .init(id: "", displayName: senderDisplayName))
|
sender: .init(id: "", displayName: senderDisplayName))
|
||||||
|
|||||||
+69
-86
@@ -230,61 +230,30 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
updateTimelineItems()
|
updateTimelineItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable:next cyclomatic_complexity
|
|
||||||
private func updateTimelineItems() {
|
private func updateTimelineItems() {
|
||||||
var newTimelineItems = [RoomTimelineItemProtocol]()
|
var newTimelineItems = [RoomTimelineItemProtocol]()
|
||||||
var canBackPaginate = true
|
var canBackPaginate = true
|
||||||
var isBackPaginating = false
|
var isBackPaginating = false
|
||||||
|
|
||||||
var createdIdentifiers = [String: Bool]()
|
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 {
|
let collapsibleChunks = timelineProvider.itemsPublisher.value.groupBy { isItemCollapsible($0) }
|
||||||
return
|
|
||||||
|
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
|
timelineItems = newTimelineItems
|
||||||
@@ -294,49 +263,63 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
callbacks.send(.isBackPaginating(isBackPaginating))
|
callbacks.send(.isBackPaginating(isBackPaginating))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func computeGroupState(for itemProxy: TimelineItemProxy,
|
private func buildTimelineItemFor(itemProxy: TimelineItemProxy,
|
||||||
previousItemProxy: TimelineItemProxy?,
|
isLastItem: Bool,
|
||||||
nextItemProxy: TimelineItemProxy?) -> TimelineItemGroupState {
|
createdIdentifiers: inout [String: Bool],
|
||||||
guard let previousItem = previousItemProxy else {
|
isBackPaginating: inout Bool,
|
||||||
// no previous item, check next item
|
canBackPaginate: inout Bool) -> RoomTimelineItemProtocol? {
|
||||||
guard let nextItem = nextItemProxy else {
|
switch itemProxy {
|
||||||
// no next item neither, this is single
|
case .event(let eventItemProxy):
|
||||||
return .single
|
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 {
|
case .virtual(let virtualItem):
|
||||||
// there is a next item but can't be grouped, this is single
|
switch virtualItem {
|
||||||
return .single
|
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
|
case .unknown:
|
||||||
return .beginning
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let nextItem = nextItemProxy else {
|
return nil
|
||||||
// no next item
|
}
|
||||||
guard itemProxy.canBeGrouped(with: previousItem) else {
|
|
||||||
// there is a previous item but can't be grouped, this is single
|
private func isItemCollapsible(_ item: TimelineItemProxy) -> Bool {
|
||||||
return .single
|
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 {
|
return false
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadVideoForTimelineItem(_ timelineItem: VideoRoomTimelineItem) async {
|
private func loadVideoForTimelineItem(_ timelineItem: VideoRoomTimelineItem) async {
|
||||||
|
|||||||
-1
@@ -20,7 +20,6 @@ import UIKit
|
|||||||
|
|
||||||
enum RoomTimelineControllerCallback {
|
enum RoomTimelineControllerCallback {
|
||||||
case updatedTimelineItems
|
case updatedTimelineItems
|
||||||
case updatedTimelineItem(_ itemId: String)
|
|
||||||
case canBackPaginate(Bool)
|
case canBackPaginate(Bool)
|
||||||
case isBackPaginating(Bool)
|
case isBackPaginating(Bool)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,20 +32,6 @@ enum TimelineItemProxy {
|
|||||||
self = .unknown(item)
|
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.
|
/// The delivery status for the item.
|
||||||
|
|||||||
@@ -17,27 +17,9 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
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 {
|
protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol, CustomStringConvertible {
|
||||||
var body: String { get }
|
var body: String { get }
|
||||||
var timestamp: String { get }
|
var timestamp: String { get }
|
||||||
var shouldShowSenderDetails: Bool { get }
|
|
||||||
var groupState: TimelineItemGroupState { get }
|
|
||||||
var isOutgoing: Bool { get }
|
var isOutgoing: Bool { get }
|
||||||
var isEditable: Bool { get }
|
var isEditable: Bool { get }
|
||||||
|
|
||||||
@@ -47,31 +29,6 @@ protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol, CustomStringC
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension EventBasedTimelineItemProtocol {
|
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 {
|
var description: String {
|
||||||
"\(String(describing: Self.self)): id: \(id), timestamp: \(timestamp), isOutgoing: \(isOutgoing), properties: \(properties)"
|
"\(String(describing: Self.self)): id: \(id), timestamp: \(timestamp), isOutgoing: \(isOutgoing), properties: \(properties)"
|
||||||
}
|
}
|
||||||
|
|||||||
-1
@@ -20,7 +20,6 @@ struct AudioRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
|
|||||||
let id: String
|
let id: String
|
||||||
let body: String
|
let body: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
let sender: TimelineItemSender
|
let sender: TimelineItemSender
|
||||||
|
|||||||
-1
@@ -21,7 +21,6 @@ struct EmoteRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
|
|||||||
let body: String
|
let body: String
|
||||||
var formattedBody: AttributedString?
|
var formattedBody: AttributedString?
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
|
||||||
|
|||||||
-1
@@ -20,7 +20,6 @@ struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hasha
|
|||||||
let id: String
|
let id: String
|
||||||
let body: String
|
let body: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
|
||||||
|
|||||||
-1
@@ -21,7 +21,6 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
|
|||||||
let id: String
|
let id: String
|
||||||
let body: String
|
let body: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
|
||||||
|
|||||||
-1
@@ -21,7 +21,6 @@ struct NoticeRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Has
|
|||||||
let body: String
|
let body: String
|
||||||
var formattedBody: AttributedString?
|
var formattedBody: AttributedString?
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
|
||||||
|
|||||||
-1
@@ -21,7 +21,6 @@ struct TextRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hasha
|
|||||||
let body: String
|
let body: String
|
||||||
var formattedBody: AttributedString?
|
var formattedBody: AttributedString?
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
|
||||||
|
|||||||
-1
@@ -20,7 +20,6 @@ struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
|
|||||||
let id: String
|
let id: String
|
||||||
let body: String
|
let body: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
|
||||||
|
|||||||
+43
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
-1
@@ -27,7 +27,6 @@ struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable,
|
|||||||
let body: String
|
let body: String
|
||||||
let encryptionType: EncryptionType
|
let encryptionType: EncryptionType
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
|
||||||
|
|||||||
-1
@@ -21,7 +21,6 @@ struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, H
|
|||||||
let id: String
|
let id: String
|
||||||
let body: String
|
let body: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
|
||||||
|
|||||||
-1
@@ -20,7 +20,6 @@ struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
|
|||||||
let id: String
|
let id: String
|
||||||
let body: String
|
let body: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
|
||||||
|
|||||||
-1
@@ -20,7 +20,6 @@ struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Ha
|
|||||||
let id: String
|
let id: String
|
||||||
let body: String
|
let body: String
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
|
||||||
|
|||||||
-1
@@ -24,7 +24,6 @@ struct UnsupportedRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable
|
|||||||
let error: String
|
let error: String
|
||||||
|
|
||||||
let timestamp: String
|
let timestamp: String
|
||||||
let groupState: TimelineItemGroupState
|
|
||||||
let isOutgoing: Bool
|
let isOutgoing: Bool
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
|
|
||||||
|
|||||||
@@ -36,53 +36,52 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable:next cyclomatic_complexity
|
// swiftlint:disable:next cyclomatic_complexity
|
||||||
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
|
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy) -> RoomTimelineItemProtocol? {
|
||||||
groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol? {
|
|
||||||
let isOutgoing = eventItemProxy.isOwn
|
let isOutgoing = eventItemProxy.isOwn
|
||||||
|
|
||||||
switch eventItemProxy.content.kind() {
|
switch eventItemProxy.content.kind() {
|
||||||
case .unableToDecrypt(let encryptedMessage):
|
case .unableToDecrypt(let encryptedMessage):
|
||||||
return buildEncryptedTimelineItem(eventItemProxy, encryptedMessage, isOutgoing, groupState)
|
return buildEncryptedTimelineItem(eventItemProxy, encryptedMessage, isOutgoing)
|
||||||
case .redactedMessage:
|
case .redactedMessage:
|
||||||
return buildRedactedTimelineItem(eventItemProxy, isOutgoing, groupState)
|
return buildRedactedTimelineItem(eventItemProxy, isOutgoing)
|
||||||
case .sticker(let body, let imageInfo, let urlString):
|
case .sticker(let body, let imageInfo, let urlString):
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
MXLog.error("Invalid sticker url string: \(urlString)")
|
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):
|
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):
|
case .failedToParseState(let eventType, _, let error):
|
||||||
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing, groupState)
|
return buildUnsupportedTimelineItem(eventItemProxy, eventType, error, isOutgoing)
|
||||||
case .message:
|
case .message:
|
||||||
guard let messageContent = eventItemProxy.content.asMessage() else { fatalError("Invalid message timeline item: \(eventItemProxy)") }
|
guard let messageContent = eventItemProxy.content.asMessage() else { fatalError("Invalid message timeline item: \(eventItemProxy)") }
|
||||||
|
|
||||||
switch messageContent.msgtype() {
|
switch messageContent.msgtype() {
|
||||||
case .text(content: let content):
|
case .text(content: let content):
|
||||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: 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):
|
case .image(content: let content):
|
||||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||||
return buildImageTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
return buildImageTimelineItemFromMessage(eventItemProxy, message, isOutgoing)
|
||||||
case .video(let content):
|
case .video(let content):
|
||||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||||
return buildVideoTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
return buildVideoTimelineItemFromMessage(eventItemProxy, message, isOutgoing)
|
||||||
case .file(let content):
|
case .file(let content):
|
||||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||||
return buildFileTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
return buildFileTimelineItemFromMessage(eventItemProxy, message, isOutgoing)
|
||||||
case .notice(let content):
|
case .notice(content: let content):
|
||||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||||
return buildNoticeTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
return buildNoticeTimelineItemFromMessage(eventItemProxy, message, isOutgoing)
|
||||||
case .emote(let content):
|
case .emote(content: let content):
|
||||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||||
return buildEmoteTimelineItemFromMessage(eventItemProxy, message, isOutgoing, groupState)
|
return buildEmoteTimelineItemFromMessage(eventItemProxy, message, isOutgoing)
|
||||||
case .audio(let content):
|
case .audio(let content):
|
||||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||||
return buildAudioTimelineItem(eventItemProxy, message, isOutgoing, groupState)
|
return buildAudioTimelineItem(eventItemProxy, message, isOutgoing)
|
||||||
case .none:
|
case .none:
|
||||||
return buildFallbackTimelineItem(eventItemProxy, isOutgoing, groupState)
|
return buildFallbackTimelineItem(eventItemProxy, isOutgoing)
|
||||||
}
|
}
|
||||||
case .state(let stateKey, let content):
|
case .state(let stateKey, let content):
|
||||||
return buildStateTimelineItemFor(eventItemProxy: eventItemProxy, state: content, stateKey: stateKey, isOutgoing: isOutgoing)
|
return buildStateTimelineItemFor(eventItemProxy: eventItemProxy, state: content, stateKey: stateKey, isOutgoing: isOutgoing)
|
||||||
@@ -98,27 +97,23 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
private func buildUnsupportedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildUnsupportedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
_ eventType: String,
|
_ eventType: String,
|
||||||
_ error: String,
|
_ error: String,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
|
||||||
UnsupportedRoomTimelineItem(id: eventItemProxy.id,
|
UnsupportedRoomTimelineItem(id: eventItemProxy.id,
|
||||||
body: ElementL10n.roomTimelineItemUnsupported,
|
body: ElementL10n.roomTimelineItemUnsupported,
|
||||||
eventType: eventType,
|
eventType: eventType,
|
||||||
error: error,
|
error: error,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
properties: RoomTimelineItemProperties())
|
properties: RoomTimelineItemProperties())
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable:next function_parameter_count
|
|
||||||
private func buildStickerTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildStickerTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
_ body: String,
|
_ body: String,
|
||||||
_ imageInfo: ImageInfo,
|
_ imageInfo: ImageInfo,
|
||||||
_ imageURL: URL,
|
_ imageURL: URL,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
|
||||||
var aspectRatio: CGFloat?
|
var aspectRatio: CGFloat?
|
||||||
let width = imageInfo.width.map(CGFloat.init)
|
let width = imageInfo.width.map(CGFloat.init)
|
||||||
let height = imageInfo.height.map(CGFloat.init)
|
let height = imageInfo.height.map(CGFloat.init)
|
||||||
@@ -129,7 +124,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
return StickerRoomTimelineItem(id: eventItemProxy.id,
|
return StickerRoomTimelineItem(id: eventItemProxy.id,
|
||||||
body: body,
|
body: body,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
@@ -144,8 +138,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
|
|
||||||
private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
_ encryptedMessage: EncryptedMessage,
|
_ encryptedMessage: EncryptedMessage,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
|
||||||
var encryptionType = EncryptedRoomTimelineItem.EncryptionType.unknown
|
var encryptionType = EncryptedRoomTimelineItem.EncryptionType.unknown
|
||||||
switch encryptedMessage {
|
switch encryptedMessage {
|
||||||
case .megolmV1AesSha2(let sessionId):
|
case .megolmV1AesSha2(let sessionId):
|
||||||
@@ -160,7 +153,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
body: ElementL10n.roomTimelineUnableToDecrypt,
|
body: ElementL10n.roomTimelineUnableToDecrypt,
|
||||||
encryptionType: encryptionType,
|
encryptionType: encryptionType,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
@@ -168,12 +160,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func buildRedactedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildRedactedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
|
||||||
RedactedRoomTimelineItem(id: eventItemProxy.id,
|
RedactedRoomTimelineItem(id: eventItemProxy.id,
|
||||||
body: ElementL10n.eventRedacted,
|
body: ElementL10n.eventRedacted,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
@@ -181,15 +171,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func buildFallbackTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildFallbackTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
|
||||||
let formattedBody = attributedStringBuilder.fromPlain(eventItemProxy.body)
|
let formattedBody = attributedStringBuilder.fromPlain(eventItemProxy.body)
|
||||||
|
|
||||||
return TextRoomTimelineItem(id: eventItemProxy.id,
|
return TextRoomTimelineItem(id: eventItemProxy.id,
|
||||||
body: eventItemProxy.body ?? "",
|
body: eventItemProxy.body ?? "",
|
||||||
formattedBody: formattedBody,
|
formattedBody: formattedBody,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
@@ -199,15 +187,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
|
|
||||||
private func buildTextTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildTextTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
_ message: MessageTimelineItem<TextMessageContent>,
|
_ message: MessageTimelineItem<TextMessageContent>,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
|
||||||
let formattedBody = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
let formattedBody = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||||
|
|
||||||
return TextRoomTimelineItem(id: message.id,
|
return TextRoomTimelineItem(id: message.id,
|
||||||
body: message.body,
|
body: message.body,
|
||||||
formattedBody: formattedBody,
|
formattedBody: formattedBody,
|
||||||
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
@@ -218,8 +204,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
|
|
||||||
private func buildImageTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildImageTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
_ message: MessageTimelineItem<ImageMessageContent>,
|
_ message: MessageTimelineItem<ImageMessageContent>,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||||
_ groupState: TimelineItemGroupState) -> 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 {
|
||||||
@@ -229,7 +214,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
return ImageRoomTimelineItem(id: message.id,
|
return ImageRoomTimelineItem(id: message.id,
|
||||||
body: message.body,
|
body: message.body,
|
||||||
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
@@ -246,8 +230,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
|
|
||||||
private func buildVideoTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildVideoTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
_ message: MessageTimelineItem<VideoMessageContent>,
|
_ message: MessageTimelineItem<VideoMessageContent>,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||||
_ groupState: TimelineItemGroupState) -> 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 {
|
||||||
@@ -257,7 +240,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
return VideoRoomTimelineItem(id: message.id,
|
return VideoRoomTimelineItem(id: message.id,
|
||||||
body: message.body,
|
body: message.body,
|
||||||
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
@@ -275,12 +257,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
|
|
||||||
private func buildAudioTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildAudioTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
_ message: MessageTimelineItem<AudioMessageContent>,
|
_ message: MessageTimelineItem<AudioMessageContent>,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
|
||||||
AudioRoomTimelineItem(id: eventItemProxy.id,
|
AudioRoomTimelineItem(id: eventItemProxy.id,
|
||||||
body: message.body,
|
body: message.body,
|
||||||
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
@@ -290,12 +270,10 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
|
|
||||||
private func buildFileTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildFileTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
_ message: MessageTimelineItem<FileMessageContent>,
|
_ message: MessageTimelineItem<FileMessageContent>,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
|
||||||
FileRoomTimelineItem(id: message.id,
|
FileRoomTimelineItem(id: message.id,
|
||||||
body: message.body,
|
body: message.body,
|
||||||
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
@@ -308,15 +286,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
|
|
||||||
private func buildNoticeTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildNoticeTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
_ message: MessageTimelineItem<NoticeMessageContent>,
|
_ message: MessageTimelineItem<NoticeMessageContent>,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
|
||||||
let formattedBody = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
let formattedBody = (message.htmlBody != nil ? attributedStringBuilder.fromHTML(message.htmlBody) : attributedStringBuilder.fromPlain(message.body))
|
||||||
|
|
||||||
return NoticeRoomTimelineItem(id: message.id,
|
return NoticeRoomTimelineItem(id: message.id,
|
||||||
body: message.body,
|
body: message.body,
|
||||||
formattedBody: formattedBody,
|
formattedBody: formattedBody,
|
||||||
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
@@ -327,8 +303,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
|
|
||||||
private func buildEmoteTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildEmoteTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
_ message: MessageTimelineItem<EmoteMessageContent>,
|
_ message: MessageTimelineItem<EmoteMessageContent>,
|
||||||
_ isOutgoing: Bool,
|
_ isOutgoing: Bool) -> RoomTimelineItemProtocol {
|
||||||
_ groupState: TimelineItemGroupState) -> RoomTimelineItemProtocol {
|
|
||||||
let name = eventItemProxy.sender.displayName ?? eventItemProxy.sender.id
|
let name = eventItemProxy.sender.displayName ?? eventItemProxy.sender.id
|
||||||
|
|
||||||
var formattedBody: AttributedString?
|
var formattedBody: AttributedString?
|
||||||
@@ -342,7 +317,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
body: message.body,
|
body: message.body,
|
||||||
formattedBody: formattedBody,
|
formattedBody: formattedBody,
|
||||||
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: message.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: groupState,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
@@ -396,7 +370,6 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
StateRoomTimelineItem(id: eventItemProxy.id,
|
StateRoomTimelineItem(id: eventItemProxy.id,
|
||||||
body: text,
|
body: text,
|
||||||
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
timestamp: eventItemProxy.timestamp.formatted(date: .omitted, time: .shortened),
|
||||||
groupState: .single,
|
|
||||||
isOutgoing: isOutgoing,
|
isOutgoing: isOutgoing,
|
||||||
isEditable: false,
|
isEditable: false,
|
||||||
sender: eventItemProxy.sender)
|
sender: eventItemProxy.sender)
|
||||||
|
|||||||
+1
-2
@@ -18,6 +18,5 @@ import Foundation
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol RoomTimelineItemFactoryProtocol {
|
protocol RoomTimelineItemFactoryProtocol {
|
||||||
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy,
|
func buildTimelineItemFor(eventItemProxy: EventTimelineItemProxy) -> RoomTimelineItemProtocol?
|
||||||
groupState: TimelineItemGroupState) -> 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-22
@@ -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
|
import SwiftUI
|
||||||
|
|
||||||
enum RoomTimelineViewProvider: Identifiable, Hashable {
|
enum RoomTimelineViewProvider: Identifiable, Hashable {
|
||||||
case text(TextRoomTimelineItem)
|
case text(TextRoomTimelineItem, TimelineGroupStyle)
|
||||||
case separator(SeparatorRoomTimelineItem)
|
case separator(SeparatorRoomTimelineItem, TimelineGroupStyle)
|
||||||
case image(ImageRoomTimelineItem)
|
case image(ImageRoomTimelineItem, TimelineGroupStyle)
|
||||||
case video(VideoRoomTimelineItem)
|
case video(VideoRoomTimelineItem, TimelineGroupStyle)
|
||||||
case audio(AudioRoomTimelineItem)
|
case audio(AudioRoomTimelineItem, TimelineGroupStyle)
|
||||||
case file(FileRoomTimelineItem)
|
case file(FileRoomTimelineItem, TimelineGroupStyle)
|
||||||
case emote(EmoteRoomTimelineItem)
|
case emote(EmoteRoomTimelineItem, TimelineGroupStyle)
|
||||||
case notice(NoticeRoomTimelineItem)
|
case notice(NoticeRoomTimelineItem, TimelineGroupStyle)
|
||||||
case redacted(RedactedRoomTimelineItem)
|
case redacted(RedactedRoomTimelineItem, TimelineGroupStyle)
|
||||||
case encrypted(EncryptedRoomTimelineItem)
|
case encrypted(EncryptedRoomTimelineItem, TimelineGroupStyle)
|
||||||
case readMarker(ReadMarkerRoomTimelineItem)
|
case readMarker(ReadMarkerRoomTimelineItem, TimelineGroupStyle)
|
||||||
case paginationIndicator(PaginationIndicatorRoomTimelineItem)
|
case paginationIndicator(PaginationIndicatorRoomTimelineItem, TimelineGroupStyle)
|
||||||
case sticker(StickerRoomTimelineItem)
|
case sticker(StickerRoomTimelineItem, TimelineGroupStyle)
|
||||||
case unsupported(UnsupportedRoomTimelineItem)
|
case unsupported(UnsupportedRoomTimelineItem, TimelineGroupStyle)
|
||||||
case timelineStart(TimelineStartRoomTimelineItem)
|
case timelineStart(TimelineStartRoomTimelineItem, TimelineGroupStyle)
|
||||||
case state(StateRoomTimelineItem)
|
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 {
|
var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .text(let item):
|
case .text(let item as RoomTimelineItemProtocol, _),
|
||||||
return item.id
|
.separator(let item as RoomTimelineItemProtocol, _),
|
||||||
case .separator(let item):
|
.image(let item as RoomTimelineItemProtocol, _),
|
||||||
return item.id
|
.video(let item as RoomTimelineItemProtocol, _),
|
||||||
case .image(let item):
|
.audio(let item as RoomTimelineItemProtocol, _),
|
||||||
return item.id
|
.file(let item as RoomTimelineItemProtocol, _),
|
||||||
case .video(let item):
|
.emote(let item as RoomTimelineItemProtocol, _),
|
||||||
return item.id
|
.notice(let item as RoomTimelineItemProtocol, _),
|
||||||
case .audio(let item):
|
.redacted(let item as RoomTimelineItemProtocol, _),
|
||||||
return item.id
|
.encrypted(let item as RoomTimelineItemProtocol, _),
|
||||||
case .file(let item):
|
.readMarker(let item as RoomTimelineItemProtocol, _),
|
||||||
return item.id
|
.paginationIndicator(let item as RoomTimelineItemProtocol, _),
|
||||||
case .emote(let item):
|
.sticker(let item as RoomTimelineItemProtocol, _),
|
||||||
return item.id
|
.unsupported(let item as RoomTimelineItemProtocol, _),
|
||||||
case .notice(let item):
|
.timelineStart(let item as RoomTimelineItemProtocol, _),
|
||||||
return item.id
|
.state(let item as RoomTimelineItemProtocol, _),
|
||||||
case .redacted(let item):
|
.group(let item as RoomTimelineItemProtocol, _):
|
||||||
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):
|
|
||||||
return item.id
|
return item.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,45 +110,77 @@ enum RoomTimelineViewProvider: Identifiable, Hashable {
|
|||||||
return false
|
return false
|
||||||
case .timelineStart, .separator, .readMarker, .paginationIndicator: // Virtual items are never reactable
|
case .timelineStart, .separator, .readMarker, .paginationIndicator: // Virtual items are never reactable
|
||||||
return false
|
return false
|
||||||
|
case .group:
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RoomTimelineViewProvider: View {
|
extension RoomTimelineViewProvider: View {
|
||||||
@ViewBuilder var body: some View {
|
var body: some View {
|
||||||
|
timelineView
|
||||||
|
.environment(\.timelineGroupStyle, timelineStyleGrouping)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private var timelineView: some View {
|
||||||
switch self {
|
switch self {
|
||||||
case .text(let item):
|
case .text(let item, _):
|
||||||
TextRoomTimelineView(timelineItem: item)
|
TextRoomTimelineView(timelineItem: item)
|
||||||
case .separator(let item):
|
case .separator(let item, _):
|
||||||
SeparatorRoomTimelineView(timelineItem: item)
|
SeparatorRoomTimelineView(timelineItem: item)
|
||||||
case .image(let item):
|
case .image(let item, _):
|
||||||
ImageRoomTimelineView(timelineItem: item)
|
ImageRoomTimelineView(timelineItem: item)
|
||||||
case .video(let item):
|
case .video(let item, _):
|
||||||
VideoRoomTimelineView(timelineItem: item)
|
VideoRoomTimelineView(timelineItem: item)
|
||||||
case .audio(let item):
|
case .audio(let item, _):
|
||||||
AudioRoomTimelineView(timelineItem: item)
|
AudioRoomTimelineView(timelineItem: item)
|
||||||
case .file(let item):
|
case .file(let item, _):
|
||||||
FileRoomTimelineView(timelineItem: item)
|
FileRoomTimelineView(timelineItem: item)
|
||||||
case .emote(let item):
|
case .emote(let item, _):
|
||||||
EmoteRoomTimelineView(timelineItem: item)
|
EmoteRoomTimelineView(timelineItem: item)
|
||||||
case .notice(let item):
|
case .notice(let item, _):
|
||||||
NoticeRoomTimelineView(timelineItem: item)
|
NoticeRoomTimelineView(timelineItem: item)
|
||||||
case .redacted(let item):
|
case .redacted(let item, _):
|
||||||
RedactedRoomTimelineView(timelineItem: item)
|
RedactedRoomTimelineView(timelineItem: item)
|
||||||
case .encrypted(let item):
|
case .encrypted(let item, _):
|
||||||
EncryptedRoomTimelineView(timelineItem: item)
|
EncryptedRoomTimelineView(timelineItem: item)
|
||||||
case .readMarker(let item):
|
case .readMarker(let item, _):
|
||||||
ReadMarkerRoomTimelineView(timelineItem: item)
|
ReadMarkerRoomTimelineView(timelineItem: item)
|
||||||
case .paginationIndicator(let item):
|
case .paginationIndicator(let item, _):
|
||||||
PaginationIndicatorRoomTimelineView(timelineItem: item)
|
PaginationIndicatorRoomTimelineView(timelineItem: item)
|
||||||
case .sticker(let item):
|
case .sticker(let item, _):
|
||||||
StickerRoomTimelineView(timelineItem: item)
|
StickerRoomTimelineView(timelineItem: item)
|
||||||
case .unsupported(let item):
|
case .unsupported(let item, _):
|
||||||
UnsupportedRoomTimelineView(timelineItem: item)
|
UnsupportedRoomTimelineView(timelineItem: item)
|
||||||
case .timelineStart(let item):
|
case .timelineStart(let item, _):
|
||||||
TimelineStartRoomTimelineView(timelineItem: item)
|
TimelineStartRoomTimelineView(timelineItem: item)
|
||||||
case .state(let item):
|
case .state(let item, _):
|
||||||
StateRoomTimelineView(timelineItem: 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ targets:
|
|||||||
- target: NSE
|
- target: NSE
|
||||||
- package: MatrixRustSDK
|
- package: MatrixRustSDK
|
||||||
- package: DesignKit
|
- package: DesignKit
|
||||||
|
- package: Algorithms
|
||||||
- package: AnalyticsEvents
|
- package: AnalyticsEvents
|
||||||
- package: AppAuth
|
- package: AppAuth
|
||||||
- package: Collections
|
- package: Collections
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
@testable import ElementX
|
||||||
|
|
||||||
|
class ArrayTests: XCTestCase {
|
||||||
|
func testGrouping() {
|
||||||
|
XCTAssertEqual([].groupBy { $0 == 0 }, [])
|
||||||
|
|
||||||
|
XCTAssertEqual([0].groupBy { $0 == 0 }, [[0]])
|
||||||
|
|
||||||
|
XCTAssertEqual([1].groupBy { $0 == 0 }, [[1]])
|
||||||
|
|
||||||
|
XCTAssertEqual([0, 0, 0].groupBy { $0 == 0 }, [[0, 0, 0]])
|
||||||
|
|
||||||
|
XCTAssertEqual([1, 1, 1].groupBy { $0 == 0 }, [[1], [1], [1]])
|
||||||
|
|
||||||
|
XCTAssertEqual([1, 0, 0, 1].groupBy { $0 == 0 }, [[1], [0, 0], [1]])
|
||||||
|
|
||||||
|
XCTAssertEqual([0, 0, 1, 0].groupBy { $0 == 0 }, [[0, 0], [1], [0]])
|
||||||
|
|
||||||
|
XCTAssertEqual([0, 0, 0, 1, 2, 3, 0].groupBy { $0 == 0 }, [[0, 0, 0], [1], [2], [3], [0]])
|
||||||
|
|
||||||
|
XCTAssertEqual([0, 0, 0, 1, 2, 3, 0, 0].groupBy { $0 == 0 }, [[0, 0, 0], [1], [2], [3], [0, 0]])
|
||||||
|
|
||||||
|
XCTAssertEqual([0, 0, 0, 1, 0, 2, 3, 0, 0].groupBy { $0 == 0 }, [[0, 0, 0], [1], [0], [2], [3], [0, 0]])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -212,23 +212,23 @@ class LoggingTests: XCTestCase {
|
|||||||
let textAttributedString = "TextAttributed"
|
let textAttributedString = "TextAttributed"
|
||||||
let textMessage = TextRoomTimelineItem(id: "mytextmessage", body: "TextString",
|
let textMessage = TextRoomTimelineItem(id: "mytextmessage", body: "TextString",
|
||||||
formattedBody: AttributedString(textAttributedString),
|
formattedBody: AttributedString(textAttributedString),
|
||||||
timestamp: "", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "sender"))
|
timestamp: "", isOutgoing: false, isEditable: false, sender: .init(id: "sender"))
|
||||||
let noticeAttributedString = "NoticeAttributed"
|
let noticeAttributedString = "NoticeAttributed"
|
||||||
let noticeMessage = NoticeRoomTimelineItem(id: "mynoticemessage", body: "NoticeString",
|
let noticeMessage = NoticeRoomTimelineItem(id: "mynoticemessage", body: "NoticeString",
|
||||||
formattedBody: AttributedString(noticeAttributedString),
|
formattedBody: AttributedString(noticeAttributedString),
|
||||||
timestamp: "", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "sender"))
|
timestamp: "", isOutgoing: false, isEditable: false, sender: .init(id: "sender"))
|
||||||
let emoteAttributedString = "EmoteAttributed"
|
let emoteAttributedString = "EmoteAttributed"
|
||||||
let emoteMessage = EmoteRoomTimelineItem(id: "myemotemessage", body: "EmoteString",
|
let emoteMessage = EmoteRoomTimelineItem(id: "myemotemessage", body: "EmoteString",
|
||||||
formattedBody: AttributedString(emoteAttributedString),
|
formattedBody: AttributedString(emoteAttributedString),
|
||||||
timestamp: "", groupState: .single, isOutgoing: false, isEditable: false, sender: .init(id: "sender"))
|
timestamp: "", isOutgoing: false, isEditable: false, sender: .init(id: "sender"))
|
||||||
let imageMessage = ImageRoomTimelineItem(id: "myimagemessage", body: "ImageString",
|
let imageMessage = ImageRoomTimelineItem(id: "myimagemessage", body: "ImageString",
|
||||||
timestamp: "", groupState: .single, isOutgoing: false, isEditable: false,
|
timestamp: "", isOutgoing: false, isEditable: false,
|
||||||
sender: .init(id: "sender"), source: nil)
|
sender: .init(id: "sender"), source: nil)
|
||||||
let videoMessage = VideoRoomTimelineItem(id: "myvideomessage", body: "VideoString",
|
let videoMessage = VideoRoomTimelineItem(id: "myvideomessage", body: "VideoString",
|
||||||
timestamp: "", groupState: .single, isOutgoing: false, isEditable: false,
|
timestamp: "", isOutgoing: false, isEditable: false,
|
||||||
sender: .init(id: "sender"), duration: 0, source: nil, thumbnailSource: nil)
|
sender: .init(id: "sender"), duration: 0, source: nil, thumbnailSource: nil)
|
||||||
let fileMessage = FileRoomTimelineItem(id: "myfilemessage", body: "FileString",
|
let fileMessage = FileRoomTimelineItem(id: "myfilemessage", body: "FileString",
|
||||||
timestamp: "", groupState: .single, isOutgoing: false, isEditable: false,
|
timestamp: "", isOutgoing: false, isEditable: false,
|
||||||
sender: .init(id: "sender"), source: nil, thumbnailSource: nil)
|
sender: .init(id: "sender"), source: nil, thumbnailSource: nil)
|
||||||
|
|
||||||
// When logging that value
|
// When logging that value
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ packages:
|
|||||||
# path: ../matrix-rust-sdk
|
# path: ../matrix-rust-sdk
|
||||||
DesignKit:
|
DesignKit:
|
||||||
path: DesignKit
|
path: DesignKit
|
||||||
|
Algorithms:
|
||||||
|
url: https://github.com/apple/swift-algorithms
|
||||||
|
majorVersion: 1.0.0
|
||||||
AnalyticsEvents:
|
AnalyticsEvents:
|
||||||
url: https://github.com/matrix-org/matrix-analytics-events
|
url: https://github.com/matrix-org/matrix-analytics-events
|
||||||
exactVersion: 0.4.0
|
exactVersion: 0.4.0
|
||||||
|
|||||||
Reference in New Issue
Block a user