diff --git a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift index 92e182c90..2d41074a4 100644 --- a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift @@ -67,10 +67,10 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { let timelineItemFactory = RoomTimelineItemFactory(userID: userID, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) - - guard let timelineController = await timelineControllerFactory.buildPinnedEventsTimelineController(roomProxy: roomProxy, - timelineItemFactory: timelineItemFactory, - mediaProvider: userSession.mediaProvider) else { + + guard case let .success(timelineController) = await timelineControllerFactory.buildPinnedEventsTimelineController(roomProxy: roomProxy, + timelineItemFactory: timelineItemFactory, + mediaProvider: userSession.mediaProvider) else { fatalError("This can never fail because we allow this view to be presented only when the timeline is fully loaded and not nil") } diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 23b6b23cb..8fff2377f 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -6270,23 +6270,6 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable { set(value) { underlyingTimeline = value } } var underlyingTimeline: TimelineProxyProtocol! - var pinnedEventsTimelineCallsCount = 0 - var pinnedEventsTimelineCalled: Bool { - return pinnedEventsTimelineCallsCount > 0 - } - - var pinnedEventsTimeline: TimelineProxyProtocol? { - get async { - pinnedEventsTimelineCallsCount += 1 - if let pinnedEventsTimelineClosure = pinnedEventsTimelineClosure { - return await pinnedEventsTimelineClosure() - } else { - return underlyingPinnedEventsTimeline - } - } - } - var underlyingPinnedEventsTimeline: TimelineProxyProtocol? - var pinnedEventsTimelineClosure: (() async -> TimelineProxyProtocol?)? var id: String { get { return underlyingId } set(value) { underlyingId = value } @@ -6578,6 +6561,70 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable { return messageFilteredTimelineFocusAllowedMessageTypesPresentationReturnValue } } + //MARK: - pinnedEventsTimeline + + var pinnedEventsTimelineUnderlyingCallsCount = 0 + var pinnedEventsTimelineCallsCount: Int { + get { + if Thread.isMainThread { + return pinnedEventsTimelineUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = pinnedEventsTimelineUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + pinnedEventsTimelineUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + pinnedEventsTimelineUnderlyingCallsCount = newValue + } + } + } + } + var pinnedEventsTimelineCalled: Bool { + return pinnedEventsTimelineCallsCount > 0 + } + + var pinnedEventsTimelineUnderlyingReturnValue: Result! + var pinnedEventsTimelineReturnValue: Result! { + get { + if Thread.isMainThread { + return pinnedEventsTimelineUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = pinnedEventsTimelineUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + pinnedEventsTimelineUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + pinnedEventsTimelineUnderlyingReturnValue = newValue + } + } + } + } + var pinnedEventsTimelineClosure: (() async -> Result)? + + func pinnedEventsTimeline() async -> Result { + pinnedEventsTimelineCallsCount += 1 + if let pinnedEventsTimelineClosure = pinnedEventsTimelineClosure { + return await pinnedEventsTimelineClosure() + } else { + return pinnedEventsTimelineReturnValue + } + } //MARK: - enableEncryption var enableEncryptionUnderlyingCallsCount = 0 @@ -15141,13 +15188,13 @@ class TimelineControllerFactoryMock: TimelineControllerFactoryProtocol, @uncheck var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)? var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)] = [] - var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue: TimelineControllerProtocol? - var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReturnValue: TimelineControllerProtocol? { + var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue: Result! + var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReturnValue: Result! { get { if Thread.isMainThread { return buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue } else { - var returnValue: TimelineControllerProtocol?? = nil + var returnValue: Result? = nil DispatchQueue.main.sync { returnValue = buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue } @@ -15165,9 +15212,9 @@ class TimelineControllerFactoryMock: TimelineControllerFactoryProtocol, @uncheck } } } - var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure: ((JoinedRoomProxyProtocol, RoomTimelineItemFactoryProtocol, MediaProviderProtocol) async -> TimelineControllerProtocol?)? + var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure: ((JoinedRoomProxyProtocol, RoomTimelineItemFactoryProtocol, MediaProviderProtocol) async -> Result)? - func buildPinnedEventsTimelineController(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol? { + func buildPinnedEventsTimelineController(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) async -> Result { buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCallsCount += 1 buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedArguments = (roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider) DispatchQueue.main.async { diff --git a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift index 126d569c3..414de9500 100644 --- a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift @@ -59,6 +59,8 @@ extension JoinedRoomProxyMock { timeline = TimelineProxyMock(.init(isAutoUpdating: configuration.shouldUseAutoUpdatingTimeline, timelineStartReached: configuration.timelineStartReached)) + + pinnedEventsTimelineReturnValue = .failure(.failedCreatingPinnedTimeline) ownUserID = configuration.ownUserID diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index 4fdd0cadf..ed9544f0e 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -445,12 +445,12 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } Task { - guard let timelineProvider = await roomProxy.pinnedEventsTimeline?.timelineProvider else { + guard case let .success(pinnedEventsTimeline) = await roomProxy.pinnedEventsTimeline() else { return } if pinnedEventsTimelineProvider == nil { - pinnedEventsTimelineProvider = timelineProvider + pinnedEventsTimelineProvider = pinnedEventsTimeline.timelineProvider } } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index a68abe709..d8f398edd 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -353,12 +353,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } Task { - guard let timelineProvider = await roomProxy.pinnedEventsTimeline?.timelineProvider else { + guard case let .success(pinnedEventsTimeline) = await roomProxy.pinnedEventsTimeline() else { return } if pinnedEventsTimelineProvider == nil { - pinnedEventsTimelineProvider = timelineProvider + pinnedEventsTimelineProvider = pinnedEventsTimeline.timelineProvider } } } diff --git a/ElementX/Sources/Services/Room/BannedRoomProxy.swift b/ElementX/Sources/Services/Room/BannedRoomProxy.swift index b928b409b..0dbf83ff9 100644 --- a/ElementX/Sources/Services/Room/BannedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/BannedRoomProxy.swift @@ -11,12 +11,14 @@ import MatrixRustSDK class BannedRoomProxy: BannedRoomProxyProtocol { private let roomListItem: RoomListItemProtocol private let roomPreview: RoomPreviewProtocol - let info: BaseRoomInfoProxyProtocol - let ownUserID: String // A room identifier is constant and lazy stops it from being fetched // multiple times over FFI lazy var id = info.id + + let ownUserID: String + + let info: BaseRoomInfoProxyProtocol init(roomListItem: RoomListItemProtocol, roomPreview: RoomPreviewProtocol, diff --git a/ElementX/Sources/Services/Room/InvitedRoomProxy.swift b/ElementX/Sources/Services/Room/InvitedRoomProxy.swift index add21b5f3..c3e9a449a 100644 --- a/ElementX/Sources/Services/Room/InvitedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/InvitedRoomProxy.swift @@ -12,13 +12,15 @@ import UIKit class InvitedRoomProxy: InvitedRoomProxyProtocol { private let roomListItem: RoomListItemProtocol private let roomPreview: RoomPreviewProtocol - let info: BaseRoomInfoProxyProtocol - let ownUserID: String - let inviter: RoomMemberProxyProtocol? // A room identifier is constant and lazy stops it from being fetched // multiple times over FFI lazy var id: String = info.id + + let ownUserID: String + + let info: BaseRoomInfoProxyProtocol + let inviter: RoomMemberProxyProtocol? init(roomListItem: RoomListItemProtocol, roomPreview: RoomPreviewProtocol, diff --git a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift index 8e8ddccf1..1fd9e704a 100644 --- a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift @@ -14,50 +14,6 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { private let roomListService: RoomListServiceProtocol private let roomListItem: RoomListItemProtocol private let room: RoomProtocol - let timeline: TimelineProxyProtocol - - private var innerPinnedEventsTimeline: TimelineProxyProtocol? - private var innerPinnedEventsTimelineTask: Task? - var pinnedEventsTimeline: TimelineProxyProtocol? { - get async { - // Check if is already available. - if let innerPinnedEventsTimeline { - return innerPinnedEventsTimeline - // Otherwise check if there is already a task loading it, and wait for it. - } else if let innerPinnedEventsTimelineTask, - let value = await innerPinnedEventsTimelineTask.value { - return value - // Else create and store a new task to load it and wait for it. - } else { - let task = Task { [weak self] in - guard let self else { - return nil - } - - do { - let sdkTimeline = try await room.timelineWithConfiguration(configuration: .init(focus: .pinnedEvents(maxEventsToLoad: 100, maxConcurrentRequests: 10), - filter: .all, - internalIdPrefix: nil, - dateDividerMode: .daily, - trackReadReceipts: false, - reportUtds: true)) - - let timeline = TimelineProxy(timeline: sdkTimeline, kind: .pinned) - - await timeline.subscribeForUpdates() - innerPinnedEventsTimeline = timeline - return timeline - } catch { - MXLog.error("Failed creating pinned events timeline with error: \(error)") - return nil - } - } - - innerPinnedEventsTimelineTask = task - return await task.value - } - } - } // periphery:ignore - required for instance retention in the rust codebase private var roomInfoObservationToken: TaskHandle? @@ -68,8 +24,19 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { // periphery:ignore - required for instance retention in the rust codebase private var knockRequestsChangesObservationToken: TaskHandle? + private var innerPinnedEventsTimeline: TimelineProxyProtocol? + private var innerPinnedEventsTimelineTask: Task, Never>? + private var subscribedForUpdates = false + // A room identifier is constant and lazy stops it from being fetched + // multiple times over FFI + lazy var id: String = room.id() + + var ownUserID: String { room.ownUserId() } + + let timeline: TimelineProxyProtocol + private let infoSubject: CurrentValueSubject var infoPublisher: CurrentValuePublisher { infoSubject.asCurrentValuePublisher() @@ -95,12 +62,6 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { knockRequestsStateSubject.asCurrentValuePublisher() } - // A room identifier is constant and lazy stops it from being fetched - // multiple times over FFI - lazy var id: String = room.id() - var ownUserID: String { room.ownUserId() } - var info: RoomInfoProxy { infoSubject.value } - init(roomListService: RoomListServiceProtocol, roomListItem: RoomListItemProtocol, room: RoomProtocol) async throws { @@ -111,7 +72,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { infoSubject = try await .init(RoomInfoProxy(roomInfo: room.roomInfo())) timeline = try await TimelineProxy(timeline: room.timelineWithConfiguration(configuration: .init(focus: .live, - filter: .eventTypeFilter(filter: eventFilters), + filter: .eventTypeFilter(filter: excludedEventsFilter), internalIdPrefix: nil, dateDividerMode: .daily, trackReadReceipts: true, @@ -131,24 +92,6 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { } } - private let eventFilters: TimelineEventTypeFilter = { - var stateEventFilters: [StateEventType] = [.roomAliases, - .roomCanonicalAlias, - .roomGuestAccess, - .roomHistoryVisibility, - .roomJoinRules, - .roomPinnedEvents, - .roomPowerLevels, - .roomServerAcl, - .roomTombstone, - .spaceChild, - .spaceParent, - .policyRuleRoom, - .policyRuleServer, - .policyRuleUser] - return .exclude(eventTypes: stateEventFilters.map { FilterTimelineEventType.state(eventType: $0) }) - }() - func subscribeForUpdates() async { guard !subscribedForUpdates else { MXLog.warning("Room already subscribed for updates") @@ -273,6 +216,43 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { } } + func pinnedEventsTimeline() async -> Result { + // Check if is already available. + if let innerPinnedEventsTimeline { + return .success(innerPinnedEventsTimeline) + // Otherwise check if there is already a task loading it, and wait for it. + } else if let innerPinnedEventsTimelineTask { + return await innerPinnedEventsTimelineTask.value + } else { // Else create and store a new task to load it and wait for it. + let task = Task, Never> { [weak self] in + guard let self else { + return .failure(.failedCreatingPinnedTimeline) + } + + do { + let sdkTimeline = try await room.timelineWithConfiguration(configuration: .init(focus: .pinnedEvents(maxEventsToLoad: 100, maxConcurrentRequests: 10), + filter: .all, + internalIdPrefix: nil, + dateDividerMode: .daily, + trackReadReceipts: false, + reportUtds: true)) + + let timeline = TimelineProxy(timeline: sdkTimeline, kind: .pinned) + + await timeline.subscribeForUpdates() + innerPinnedEventsTimeline = timeline + return .success(timeline) + } catch { + MXLog.error("Failed creating pinned events timeline with error: \(error)") + return .failure(.sdkError(error)) + } + } + + innerPinnedEventsTimelineTask = task + return await task.value + } + } + func enableEncryption() async -> Result { do { try await room.enableEncryption() @@ -856,6 +836,24 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { MXLog.error("Failed observing requests to join with error: \(error)") } } + + private let excludedEventsFilter: TimelineEventTypeFilter = { + var stateEventFilters: [StateEventType] = [.roomAliases, + .roomCanonicalAlias, + .roomGuestAccess, + .roomHistoryVisibility, + .roomJoinRules, + .roomPinnedEvents, + .roomPowerLevels, + .roomServerAcl, + .roomTombstone, + .spaceChild, + .spaceParent, + .policyRuleRoom, + .policyRuleServer, + .policyRuleUser] + return .exclude(eventTypes: stateEventFilters.map { FilterTimelineEventType.state(eventType: $0) }) + }() } private final class RoomTypingNotificationUpdateListener: TypingNotificationsListener { diff --git a/ElementX/Sources/Services/Room/KnockedRoomProxy.swift b/ElementX/Sources/Services/Room/KnockedRoomProxy.swift index 9067d20e4..a6a8e8e0e 100644 --- a/ElementX/Sources/Services/Room/KnockedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/KnockedRoomProxy.swift @@ -11,12 +11,14 @@ import MatrixRustSDK class KnockedRoomProxy: KnockedRoomProxyProtocol { private let roomListItem: RoomListItemProtocol private let roomPreview: RoomPreviewProtocol - let info: BaseRoomInfoProxyProtocol - let ownUserID: String // A room identifier is constant and lazy stops it from being fetched // multiple times over FFI lazy var id = info.id + + let ownUserID: String + + let info: BaseRoomInfoProxyProtocol init(roomListItem: RoomListItemProtocol, roomPreview: RoomPreviewProtocol, diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index 67e073445..1ddc13690 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -16,6 +16,7 @@ enum RoomProxyError: Error { case invalidMedia case eventNotFound case missingTransactionID + case failedCreatingPinnedTimeline } /// An enum that describes the relationship between the current user and the room, and contains a reference to the specific implementation of the `RoomProxy`. @@ -75,8 +76,6 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol { var timeline: TimelineProxyProtocol { get } - var pinnedEventsTimeline: TimelineProxyProtocol? { get async } - func subscribeForUpdates() async func subscribeToRoomInfoUpdates() @@ -89,6 +88,8 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol { allowedMessageTypes: [TimelineAllowedMessageType], presentation: TimelineKind.MediaPresentation) async -> Result + func pinnedEventsTimeline() async -> Result + func enableEncryption() async -> Result func redact(_ eventID: String) async -> Result diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerFactory.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerFactory.swift index dee4c4d03..5b34ac712 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerFactory.swift @@ -40,17 +40,18 @@ struct TimelineControllerFactory: TimelineControllerFactoryProtocol { func buildPinnedEventsTimelineController(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, - mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol? { - guard let pinnedEventsTimeline = await roomProxy.pinnedEventsTimeline else { - return nil + mediaProvider: MediaProviderProtocol) async -> Result { + switch await roomProxy.pinnedEventsTimeline() { + case .success(let timelineProxy): + return .success(TimelineController(roomProxy: roomProxy, + timelineProxy: timelineProxy, + initialFocussedEventID: nil, + timelineItemFactory: timelineItemFactory, + mediaProvider: mediaProvider, + appSettings: ServiceLocator.shared.settings)) + case .failure(let error): + return .failure(.roomProxyError(error)) } - - return TimelineController(roomProxy: roomProxy, - timelineProxy: pinnedEventsTimeline, - initialFocussedEventID: nil, - timelineItemFactory: timelineItemFactory, - mediaProvider: mediaProvider, - appSettings: ServiceLocator.shared.settings) } func buildMessageFilteredTimelineController(focus: TimelineFocus, diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerFactoryProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerFactoryProtocol.swift index e9c9c53ce..f0bb98cab 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerFactoryProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerFactoryProtocol.swift @@ -26,7 +26,7 @@ protocol TimelineControllerFactoryProtocol { func buildPinnedEventsTimelineController(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, - mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol? + mediaProvider: MediaProviderProtocol) async -> Result func buildMessageFilteredTimelineController(focus: TimelineFocus, allowedMessageTypes: [TimelineAllowedMessageType], diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index 537d46637..0b68be1ff 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -30,7 +30,11 @@ class RoomScreenViewModelTests: XCTestCase { let roomProxyMock = JoinedRoomProxyMock(configuration) // setup a way to inject the mock of the pinned events timeline roomProxyMock.pinnedEventsTimelineClosure = { - await timelineSubject.values.first() + guard let timeline = await timelineSubject.values.first() else { + fatalError() + } + + return .success(timeline) } // setup the room proxy actions publisher roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() @@ -113,7 +117,7 @@ class RoomScreenViewModelTests: XCTestCase { pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init("1"))), .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init("2"))), .event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: .init("3")))] - roomProxyMock.underlyingPinnedEventsTimeline = pinnedTimelineMock + roomProxyMock.pinnedEventsTimelineReturnValue = .success(pinnedTimelineMock) let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(), roomProxy: roomProxyMock, initialSelectedPinnedEventID: "test1",