// // Copyright 2025 Element Creations Ltd. // Copyright 2022-2025 New Vector Ltd. // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. // Please see LICENSE files in the repository root for full details. // import Combine @testable import ElementX import Foundation import MatrixRustSDK import Testing @MainActor final class TimelineViewModelTests { var userIndicatorControllerMock: UserIndicatorControllerMock! var cancellables = Set() init() async throws { AppSettings.resetAllSettings() cancellables.removeAll() userIndicatorControllerMock = UserIndicatorControllerMock.default } deinit { userIndicatorControllerMock = nil } // MARK: - Message Grouping @Test func messageGrouping() { // Given 3 messages from Bob. let items = [ TextRoomTimelineItem(text: "Message 1", sender: "bob"), TextRoomTimelineItem(text: "Message 2", sender: "bob"), TextRoomTimelineItem(text: "Message 3", sender: "bob") ] // When showing them in a timeline. let timelineController = MockTimelineController() timelineController.timelineItems = items let viewModel = makeViewModel(timelineController: timelineController) // Then the messages should be grouped together. #expect(viewModel.state.timelineState.itemViewStates[0].groupStyle == .first, "Nothing should prevent the first message from being grouped.") #expect(viewModel.state.timelineState.itemViewStates[1].groupStyle == .middle, "Nothing should prevent the middle message from being grouped.") #expect(viewModel.state.timelineState.itemViewStates[2].groupStyle == .last, "Nothing should prevent the last message from being grouped.") } @Test func messageGroupingMultipleSenders() { // Given some interleaved messages from Bob and Alice. let items = [ TextRoomTimelineItem(text: "Message 1", sender: "alice"), TextRoomTimelineItem(text: "Message 2", sender: "bob"), TextRoomTimelineItem(text: "Message 3", sender: "alice"), TextRoomTimelineItem(text: "Message 4", sender: "alice"), TextRoomTimelineItem(text: "Message 5", sender: "bob"), TextRoomTimelineItem(text: "Message 6", sender: "bob") ] // When showing them in a timeline. let timelineController = MockTimelineController() timelineController.timelineItems = items let viewModel = makeViewModel(timelineController: timelineController) // Then the messages should be grouped by sender. #expect(viewModel.state.timelineState.itemViewStates[0].groupStyle == .single, "A message should not be grouped when the sender changes.") #expect(viewModel.state.timelineState.itemViewStates[1].groupStyle == .single, "A message should not be grouped when the sender changes.") #expect(viewModel.state.timelineState.itemViewStates[2].groupStyle == .first, "A group should start with a new sender if there are more messages from that sender.") #expect(viewModel.state.timelineState.itemViewStates[3].groupStyle == .last, "A group should be ended when the sender changes in the next message.") #expect(viewModel.state.timelineState.itemViewStates[4].groupStyle == .first, "A group should start with a new sender if there are more messages from that sender.") #expect(viewModel.state.timelineState.itemViewStates[5].groupStyle == .last, "A group should be ended when the sender changes in the next message.") } @Test func messageGroupingWithLeadingReactions() { // Given 3 messages from Bob where the first message has a reaction. let items = [ TextRoomTimelineItem(text: "Message 1", sender: "bob", addReactions: true), TextRoomTimelineItem(text: "Message 2", sender: "bob"), TextRoomTimelineItem(text: "Message 3", sender: "bob") ] // When showing them in a timeline. let timelineController = MockTimelineController() timelineController.timelineItems = items let viewModel = makeViewModel(timelineController: timelineController) // Then the first message should not be grouped but the other two should. #expect(viewModel.state.timelineState.itemViewStates[0].groupStyle == .single, "When the first message has reactions it should not be grouped.") #expect(viewModel.state.timelineState.itemViewStates[1].groupStyle == .first, "A new group should be made when the preceding message has reactions.") #expect(viewModel.state.timelineState.itemViewStates[2].groupStyle == .last, "Nothing should prevent the last message from being grouped.") } @Test func messageGroupingWithInnerReactions() { // Given 3 messages from Bob where the middle message has a reaction. let items = [ TextRoomTimelineItem(text: "Message 1", sender: "bob"), TextRoomTimelineItem(text: "Message 2", sender: "bob", addReactions: true), TextRoomTimelineItem(text: "Message 3", sender: "bob") ] // When showing them in a timeline. let timelineController = MockTimelineController() timelineController.timelineItems = items let viewModel = makeViewModel(timelineController: timelineController) // Then the first and second messages should be grouped and the last one should not. #expect(viewModel.state.timelineState.itemViewStates[0].groupStyle == .first, "Nothing should prevent the first message from being grouped.") #expect(viewModel.state.timelineState.itemViewStates[1].groupStyle == .last, "When the message has reactions, the group should end here.") #expect(viewModel.state.timelineState.itemViewStates[2].groupStyle == .single, "The last message should not be grouped when the preceding message has reactions.") } @Test func messageGroupingWithTrailingReactions() { // Given 3 messages from Bob where the last message has a reaction. let items = [ TextRoomTimelineItem(text: "Message 1", sender: "bob"), TextRoomTimelineItem(text: "Message 2", sender: "bob"), TextRoomTimelineItem(text: "Message 3", sender: "bob", addReactions: true) ] // When showing them in a timeline. let timelineController = MockTimelineController() timelineController.timelineItems = items let viewModel = makeViewModel(timelineController: timelineController) // Then the messages should be grouped together. #expect(viewModel.state.timelineState.itemViewStates[0].groupStyle == .first, "Nothing should prevent the first message from being grouped.") #expect(viewModel.state.timelineState.itemViewStates[1].groupStyle == .middle, "Nothing should prevent the second message from being grouped.") #expect(viewModel.state.timelineState.itemViewStates[2].groupStyle == .last, "Reactions on the last message should not prevent it from being grouped.") } // MARK: - Focussing @Test func focusItem() async throws { // Given a room with 3 items loaded in a live timeline. let items = [TextRoomTimelineItem(eventID: "t1"), TextRoomTimelineItem(eventID: "t2"), TextRoomTimelineItem(eventID: "t3")] let timelineController = MockTimelineController() timelineController.timelineItems = items let viewModel = makeViewModel(timelineController: timelineController) #expect(timelineController.focusOnEventCallCount == 0) #expect(viewModel.context.viewState.timelineState.isLive) #expect(viewModel.context.viewState.timelineState.focussedEvent == nil) // When focussing on an item that isn't loaded. let deferred = deferFulfillment(viewModel.context.$viewState) { !$0.timelineState.isLive } await viewModel.focusOnEvent(eventID: "t4") try await deferred.fulfill() // Then a new timeline should be loaded and the room focussed on that event. #expect(timelineController.focusOnEventCallCount == 1) #expect(!viewModel.context.viewState.timelineState.isLive) #expect(viewModel.context.viewState.timelineState.focussedEvent == .init(eventID: "t4", appearance: .immediate)) } @Test func focusLoadedItem() async throws { // Given a room with 3 items loaded in a live timeline. let items = [TextRoomTimelineItem(eventID: "t1"), TextRoomTimelineItem(eventID: "t2"), TextRoomTimelineItem(eventID: "t3")] let timelineController = MockTimelineController() timelineController.timelineItems = items let viewModel = makeViewModel(timelineController: timelineController) #expect(timelineController.focusOnEventCallCount == 0) #expect(viewModel.context.viewState.timelineState.isLive) #expect(viewModel.context.viewState.timelineState.focussedEvent == nil) // When focussing on a loaded item. let deferred = deferFailure(viewModel.context.$viewState, timeout: .seconds(1)) { !$0.timelineState.isLive } await viewModel.focusOnEvent(eventID: "t1") try await deferred.fulfill() // Then the timeline should remain live and the item should be focussed. #expect(timelineController.focusOnEventCallCount == 0) #expect(viewModel.context.viewState.timelineState.isLive) #expect(viewModel.context.viewState.timelineState.focussedEvent == .init(eventID: "t1", appearance: .animated)) } @Test func focusLive() async throws { // Given a room with a non-live timeline focussed on a particular event. let items = [TextRoomTimelineItem(eventID: "t1"), TextRoomTimelineItem(eventID: "t2"), TextRoomTimelineItem(eventID: "t3")] let timelineController = MockTimelineController() timelineController.timelineItems = items let viewModel = makeViewModel(timelineController: timelineController) var deferred = deferFulfillment(viewModel.context.$viewState) { !$0.timelineState.isLive } await viewModel.focusOnEvent(eventID: "t4") try await deferred.fulfill() #expect(timelineController.focusLiveCallCount == 0) #expect(!viewModel.context.viewState.timelineState.isLive) #expect(viewModel.context.viewState.timelineState.focussedEvent == .init(eventID: "t4", appearance: .immediate)) // When switching back to a live timeline. deferred = deferFulfillment(viewModel.context.$viewState) { $0.timelineState.isLive } viewModel.context.send(viewAction: .focusLive) try await deferred.fulfill() // Then the timeline should switch back to being live and the event focus should be removed. #expect(timelineController.focusLiveCallCount == 1) #expect(viewModel.context.viewState.timelineState.isLive) #expect(viewModel.context.viewState.timelineState.focussedEvent == nil) } @Test func initialFocusViewState() { let timelineController = MockTimelineController() let viewModel = makeViewModel(focussedEventID: "t10", timelineController: timelineController) #expect(viewModel.context.viewState.timelineState.focussedEvent == .init(eventID: "t10", appearance: .immediate)) } // MARK: - Read Receipts @Test func sendReadReceipt() async throws { // Given a room with only text items in the timeline let items = [TextRoomTimelineItem(eventID: "t1"), TextRoomTimelineItem(eventID: "t2"), TextRoomTimelineItem(eventID: "t3")] let (viewModel, _, timelineProxy, _) = readReceiptsConfiguration(with: items) // When sending a read receipt for the last item. try viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(#require(items.last?.id))) try await Task.sleep(for: .milliseconds(100)) // Then the receipt should be sent. #expect(timelineProxy.sendReadReceiptForTypeCalled == true) let arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments #expect(arguments?.eventID == "t3") #expect(arguments?.type == .read) } @Test func sendReadReceiptWithoutEvents() async throws { // Given a room with only virtual items. let items = [SeparatorRoomTimelineItem(uniqueID: .init("v1")), SeparatorRoomTimelineItem(uniqueID: .init("v2")), SeparatorRoomTimelineItem(uniqueID: .init("v3"))] let (viewModel, _, timelineProxy, _) = readReceiptsConfiguration(with: items) // When sending a read receipt for the last item. try viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(#require(items.last?.id))) try await Task.sleep(for: .milliseconds(100)) // Then nothing should be sent. #expect(timelineProxy.sendReadReceiptForTypeCalled == false) } @Test func sendReadReceiptVirtualLast() async throws { // Given a room where the last event is a virtual item. let items: [RoomTimelineItemProtocol] = [TextRoomTimelineItem(eventID: "t1"), TextRoomTimelineItem(eventID: "t2"), SeparatorRoomTimelineItem(uniqueID: .init("v3"))] let (viewModel, _, _, _) = readReceiptsConfiguration(with: items) // When sending a read receipt for the last item. try viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(#require(items.last?.id))) try await Task.sleep(for: .milliseconds(100)) } // swiftlint:disable:next large_tuple private func readReceiptsConfiguration(with items: [RoomTimelineItemProtocol]) -> (TimelineViewModel, JoinedRoomProxyMock, TimelineProxyMock, MockTimelineController) { let roomProxy = JoinedRoomProxyMock(.init(name: "")) let timelineProxy = TimelineProxyMock() roomProxy.timeline = timelineProxy let timelineController = MockTimelineController() timelineProxy.sendReadReceiptForTypeReturnValue = .success(()) timelineController.timelineItems = items timelineController.roomProxy = roomProxy let viewModel = TimelineViewModel(roomProxy: roomProxy, timelineController: timelineController, userSession: UserSessionMock(.init()), mediaPlayerProvider: MediaPlayerProviderMock(), userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) return (viewModel, roomProxy, timelineProxy, timelineController) } @Test func showReadReceipts() async throws { let receipts: [ReadReceipt] = [.init(userID: "@alice:matrix.org", formattedTimestamp: "12:00"), .init(userID: "@charlie:matrix.org", formattedTimestamp: "11:00")] // Given 3 messages from Bob where the middle message has a reaction. let message = TextRoomTimelineItem(text: "Test", sender: "bob", addReadReceipts: receipts) let id = message.id // When showing them in a timeline. let timelineController = MockTimelineController() timelineController.timelineItems = [message] let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAlice, RoomMemberProxyMock.mockCharlie])), timelineController: timelineController, userSession: UserSessionMock(.init()), mediaPlayerProvider: MediaPlayerProviderMock(), userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) let deferred = deferFulfillment(viewModel.context.$viewState) { value in value.bindings.readReceiptsSummaryInfo?.orderedReceipts == receipts } viewModel.context.send(viewAction: .displayReadReceipts(itemID: id)) try await deferred.fulfill() } @Test func showManageUserAsAdmin() async throws { let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice], ownUserID: RoomMemberProxyMock.mockAdmin.userID)), timelineController: MockTimelineController(), userSession: UserSessionMock(.init()), mediaPlayerProvider: MediaPlayerProviderMock(), userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) var deferred = deferFulfillment(viewModel.context.$viewState) { value in value.canCurrentUserKick && value.canCurrentUserBan } try await deferred.fulfill() deferred = deferFulfillment(viewModel.context.$viewState) { value in value.bindings.manageMemberViewModel != nil } viewModel.context.send(viewAction: .tappedOnSenderDetails(sender: .init(with: RoomMemberProxyMock.mockAlice))) try await deferred.fulfill() #expect(viewModel.context.manageMemberViewModel?.id == RoomMemberProxyMock.mockAlice.userID) #expect(viewModel.context.manageMemberViewModel?.state.permissions.canBan == true) #expect(viewModel.context.manageMemberViewModel?.state.permissions.canKick == true) #expect(viewModel.context.manageMemberViewModel?.state.isKickDisabled == false) #expect(viewModel.context.manageMemberViewModel?.state.isBanUnbanDisabled == false) } @Test func showDetailsForAnAdmin() async throws { let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice], ownUserID: RoomMemberProxyMock.mockAlice.userID)), timelineController: MockTimelineController(), userSession: UserSessionMock(.init()), mediaPlayerProvider: MediaPlayerProviderMock(), userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) var deferredState = deferFulfillment(viewModel.context.$viewState) { value in !value.canCurrentUserKick && !value.canCurrentUserBan } try await deferredState.fulfill() deferredState = deferFulfillment(viewModel.context.$viewState) { value in value.bindings.manageMemberViewModel != nil } viewModel.context.send(viewAction: .tappedOnSenderDetails(sender: .init(with: RoomMemberProxyMock.mockAdmin))) try await deferredState.fulfill() #expect(viewModel.context.manageMemberViewModel?.state.permissions.canBan == false) #expect(viewModel.context.manageMemberViewModel?.state.permissions.canKick == false) #expect(viewModel.context.manageMemberViewModel?.state.isKickDisabled == true) #expect(viewModel.context.manageMemberViewModel?.state.isBanUnbanDisabled == true) #expect(viewModel.context.manageMemberViewModel?.id == RoomMemberProxyMock.mockAdmin.userID) } @Test func showDetailsForABannedUser() async throws { let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockBanned[0]], ownUserID: RoomMemberProxyMock.mockAdmin.userID)), timelineController: MockTimelineController(), userSession: UserSessionMock(.init()), mediaPlayerProvider: MediaPlayerProviderMock(), userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) var deferredState = deferFulfillment(viewModel.context.$viewState) { value in value.canCurrentUserKick && value.canCurrentUserBan } try await deferredState.fulfill() deferredState = deferFulfillment(viewModel.context.$viewState) { value in value.bindings.manageMemberViewModel != nil } viewModel.context.send(viewAction: .tappedOnSenderDetails(sender: .init(with: RoomMemberProxyMock.mockBanned[0]))) try await deferredState.fulfill() #expect(viewModel.context.manageMemberViewModel?.state.permissions.canBan == true) #expect(viewModel.context.manageMemberViewModel?.state.permissions.canKick == true) #expect(viewModel.context.manageMemberViewModel?.state.isKickDisabled == true) #expect(viewModel.context.manageMemberViewModel?.state.isBanUnbanDisabled == false) #expect(viewModel.context.manageMemberViewModel?.state.isMemberBanned == true) #expect(viewModel.context.manageMemberViewModel?.id == RoomMemberProxyMock.mockBanned[0].userID) } // MARK: - Pins @Test func pinnedEvents() async throws { var configuration = JoinedRoomProxyMockConfiguration(name: "", pinnedEventIDs: .init(["test1"])) let roomProxyMock = JoinedRoomProxyMock(configuration) let infoSubject = CurrentValueSubject(RoomInfoProxyMock(configuration)) roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() let viewModel = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockTimelineController(), userSession: UserSessionMock(.init()), mediaPlayerProvider: MediaPlayerProviderMock(), userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) #expect(configuration.pinnedEventIDs == viewModel.context.viewState.pinnedEventIDs) configuration.pinnedEventIDs = ["test1", "test2"] let deferred = deferFulfillment(viewModel.context.$viewState) { value in value.pinnedEventIDs == ["test1", "test2"] } infoSubject.send(RoomInfoProxyMock(configuration)) try await deferred.fulfill() } @Test func canUserPinEvents() async throws { let configuration = JoinedRoomProxyMockConfiguration(name: "", powerLevelsConfiguration: .init(canUserPin: true)) let roomProxyMock = JoinedRoomProxyMock(configuration) let infoSubject = CurrentValueSubject(RoomInfoProxyMock(configuration)) roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() let viewModel = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockTimelineController(), userSession: UserSessionMock(.init()), mediaPlayerProvider: MediaPlayerProviderMock(), userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) var deferred = deferFulfillment(viewModel.context.$viewState) { value in value.canCurrentUserPin } try await deferred.fulfill() let powerLevelsProxyMock = RoomPowerLevelsProxyMock(configuration: .init()) powerLevelsProxyMock.canUserPinOrUnpinUserIDReturnValue = .success(false) powerLevelsProxyMock.canOwnUserPinOrUnpinReturnValue = false roomProxyMock.powerLevelsReturnValue = .success(powerLevelsProxyMock) let roomInfoProxyMock = RoomInfoProxyMock(configuration) roomInfoProxyMock.powerLevels = powerLevelsProxyMock deferred = deferFulfillment(viewModel.context.$viewState) { value in !value.canCurrentUserPin } infoSubject.send(roomInfoProxyMock) try await deferred.fulfill() } // MARK: - Tap Actions @Test func tapSendInfoEncryptionAuthentictyDisplaysAlert() { // Given a room with an event whose authenticity could not be verified let items = [TextRoomTimelineItem(eventID: "t1", encryptionAuthenticity: .verificationViolation(color: .red))] let timelineController = MockTimelineController() timelineController.timelineItems = items let viewModel = makeViewModel(timelineController: timelineController) #expect(viewModel.state.bindings.alertInfo == nil) viewModel.process(viewAction: .itemSendInfoTapped(itemID: items[0].id)) #expect(viewModel.state.bindings.alertInfo?.title == "Encrypted by a previously-verified user.") } @Test func tapSendInfoEncryptionForwarderDisplaysAlert() { // Given a room with an event whose key was forwarded let items = [TextRoomTimelineItem(eventID: "t1", keyForwarder: .test)] let timelineController = MockTimelineController() timelineController.timelineItems = items let viewModel = makeViewModel(timelineController: timelineController) #expect(viewModel.state.bindings.alertInfo == nil) viewModel.process(viewAction: .itemSendInfoTapped(itemID: items[0].id)) #expect(viewModel.state.bindings.alertInfo?.title == "alice (@alice:matrix.org) shared this message since you were not in the room when it was sent.") } // MARK: - Helpers private func makeViewModel(roomProxy: JoinedRoomProxyProtocol? = nil, focussedEventID: String? = nil, timelineController: TimelineControllerProtocol) -> TimelineViewModel { TimelineViewModel(roomProxy: roomProxy ?? JoinedRoomProxyMock(.init(name: "")), focussedEventID: focussedEventID, timelineController: timelineController, userSession: UserSessionMock(.init()), mediaPlayerProvider: MediaPlayerProviderMock(), userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, analyticsService: ServiceLocator.shared.analytics, emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) } } private extension TextRoomTimelineItem { init(text: String, sender: String, addReactions: Bool = false, addReadReceipts: [ReadReceipt] = []) { let reactions = addReactions ? [AggregatedReaction(accountOwnerID: "bob", key: "🦄", senders: [ReactionSender(id: sender, timestamp: Date())])] : [] self.init(id: .randomEvent, timestamp: .mock, isOutgoing: sender == "bob", isEditable: sender == "bob", canBeRepliedTo: true, sender: .init(id: "@\(sender):server.com", displayName: sender), content: .init(body: text), properties: RoomTimelineItemProperties(reactions: reactions, orderedReadReceipts: addReadReceipts)) } } private extension SeparatorRoomTimelineItem { init(uniqueID: TimelineItemIdentifier.UniqueID) { self.init(id: .virtual(uniqueID: uniqueID), timestamp: .mock) } } private extension TextRoomTimelineItem { init(eventID: String) { self.init(id: .event(uniqueID: .init(UUID().uuidString), eventOrTransactionID: .eventID(eventID)), timestamp: .mock, isOutgoing: false, isEditable: false, canBeRepliedTo: true, sender: .init(id: ""), content: .init(body: "Hello, World!")) } } private extension TextRoomTimelineItem { init(eventID: String, keyForwarder: TimelineItemKeyForwarder) { self.init(id: .event(uniqueID: .init(UUID().uuidString), eventOrTransactionID: .eventID(eventID)), timestamp: .mock, isOutgoing: false, isEditable: false, canBeRepliedTo: true, sender: .init(id: ""), content: .init(body: "Hello, World!"), properties: RoomTimelineItemProperties(encryptionForwarder: keyForwarder)) } } private extension TextRoomTimelineItem { init(eventID: String, encryptionAuthenticity: EncryptionAuthenticity) { self.init(id: .event(uniqueID: .init(UUID().uuidString), eventOrTransactionID: .eventID(eventID)), timestamp: .mock, isOutgoing: false, isEditable: false, canBeRepliedTo: true, sender: .init(id: ""), content: .init(body: "Hello, World!"), properties: RoomTimelineItemProperties(encryptionAuthenticity: encryptionAuthenticity)) } } private extension TimelineItemSender { init(with proxy: RoomMemberProxyMock) { self.init(id: proxy.userID, displayName: proxy.displayName ?? "", isDisplayNameAmbiguous: false, avatarURL: proxy.avatarURL) } } private extension TimelineItemKeyForwarder { static var test: TimelineItemKeyForwarder { TimelineItemKeyForwarder(id: "@alice:matrix.org", displayName: "alice") } }