From 4f38386ec995b1d137599821a9f139055e8aa191 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Mon, 29 Jul 2024 17:45:41 +0200 Subject: [PATCH 1/8] timeline is used for the banner --- .../RoomFlowCoordinator.swift | 7 ++ .../Mocks/Generated/GeneratedMocks.swift | 53 +++++++------ .../Mocks/Generated/SDKGeneratedMocks.swift | 75 +++++++++++++++++++ .../RoomTimelineControllerFactoryMock.swift | 2 +- .../RoomScreen/RoomScreenCoordinator.swift | 2 + .../Screens/RoomScreen/RoomScreenModels.swift | 22 +++--- .../RoomScreen/RoomScreenViewModel.swift | 60 +++++++++++---- .../PinnedItemsBannerView.swift | 6 +- .../ReadReceiptsSummaryView.swift | 1 + .../Screens/RoomScreen/View/RoomScreen.swift | 3 +- .../Style/TimelineItemBubbledStylerView.swift | 2 +- .../TimelineReadReceiptsView.swift | 1 + .../HighlightedTimelineItemModifier.swift | 2 +- .../View/Timeline/TimelineView.swift | 1 + .../Sources/Services/Room/RoomProxy.swift | 3 + .../Services/Room/RoomProxyProtocol.swift | 2 + .../RoomTimelineController.swift | 5 +- .../RoomTimelineControllerFactory.swift | 2 + ...oomTimelineControllerFactoryProtocol.swift | 1 + .../UITests/UITestsAppCoordinator.swift | 14 ++++ 20 files changed, 206 insertions(+), 58 deletions(-) diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 81619663ca..ef8dc56566 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -83,6 +83,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } private var timelineController: RoomTimelineControllerProtocol? + private var pinnedItemsTimelineController: RoomTimelineControllerProtocol? init(roomID: String, userSession: UserSessionProtocol, @@ -545,10 +546,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy, + timelineProxy: roomProxy.timeline, initialFocussedEventID: focussedEventID, timelineItemFactory: timelineItemFactory) self.timelineController = timelineController + let pinnedItemsTimelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy, timelineProxy: roomProxy.pinnedEventsTimeline, initialFocussedEventID: nil, timelineItemFactory: timelineItemFactory) + self.pinnedItemsTimelineController = pinnedItemsTimelineController + analytics.trackViewRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace) let completionSuggestionService = CompletionSuggestionService(roomProxy: roomProxy) @@ -558,6 +563,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy, focussedEventID: focussedEventID, timelineController: timelineController, + pinnedEventsTimelineController: pinnedItemsTimelineController, mediaProvider: userSession.mediaProvider, mediaPlayerProvider: MediaPlayerProvider(), voiceMessageMediaManager: userSession.voiceMessageMediaManager, @@ -1039,6 +1045,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) let roomTimelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy, + timelineProxy: roomProxy.timeline, initialFocussedEventID: nil, timelineItemFactory: timelineItemFactory) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 8dad86fca9..55482daf57 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -8403,6 +8403,11 @@ class RoomProxyMock: RoomProxyProtocol { set(value) { underlyingTimeline = value } } var underlyingTimeline: TimelineProxyProtocol! + var pinnedEventsTimeline: TimelineProxyProtocol { + get { return underlyingPinnedEventsTimeline } + set(value) { underlyingPinnedEventsTimeline = value } + } + var underlyingPinnedEventsTimeline: TimelineProxyProtocol! //MARK: - subscribeForUpdates @@ -11461,15 +11466,15 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { //MARK: - buildRoomTimelineController - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = 0 - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount: Int { + var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = 0 + var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryCallsCount: Int { get { if Thread.isMainThread { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount + return buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount + returnValue = buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount } return returnValue! @@ -11477,29 +11482,29 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { } set { if Thread.isMainThread { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = newValue + buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = newValue + buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = newValue } } } } - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCalled: Bool { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount > 0 + var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryCalled: Bool { + return buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryCallsCount > 0 } - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments: (roomProxy: RoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol)? - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedInvocations: [(roomProxy: RoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol)] = [] + var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments: (roomProxy: RoomProxyProtocol, timelineProxy: TimelineProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol)? + var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryReceivedInvocations: [(roomProxy: RoomProxyProtocol, timelineProxy: TimelineProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol)] = [] - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue: RoomTimelineControllerProtocol! - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue: RoomTimelineControllerProtocol! { + var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue: RoomTimelineControllerProtocol! + var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryReturnValue: RoomTimelineControllerProtocol! { get { if Thread.isMainThread { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue + return buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue } else { var returnValue: RoomTimelineControllerProtocol? = nil DispatchQueue.main.sync { - returnValue = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue + returnValue = buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue } return returnValue! @@ -11507,26 +11512,26 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { } set { if Thread.isMainThread { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue = newValue + buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue = newValue + buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue = newValue } } } } - var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure: ((RoomProxyProtocol, String?, RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol)? + var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryClosure: ((RoomProxyProtocol, TimelineProxyProtocol, String?, RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol)? - func buildRoomTimelineController(roomProxy: RoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol { - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount += 1 - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments = (roomProxy: roomProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory) + func buildRoomTimelineController(roomProxy: RoomProxyProtocol, timelineProxy: TimelineProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol { + buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryCallsCount += 1 + buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments = (roomProxy: roomProxy, timelineProxy: timelineProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory) DispatchQueue.main.async { - self.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedInvocations.append((roomProxy: roomProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory)) + self.buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryReceivedInvocations.append((roomProxy: roomProxy, timelineProxy: timelineProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory)) } - if let buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure(roomProxy, initialFocussedEventID, timelineItemFactory) + if let buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryClosure = buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryClosure { + return buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryClosure(roomProxy, timelineProxy, initialFocussedEventID, timelineItemFactory) } else { - return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue + return buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryReturnValue } } } diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index 5142c895ab..7db135cb5a 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -13005,6 +13005,81 @@ open class RoomSDKMock: MatrixRustSDK.Room { } } + //MARK: - pinnedEventsTimeline + + open var pinnedEventsTimelineInternalIdPrefixThrowableError: Error? + var pinnedEventsTimelineInternalIdPrefixUnderlyingCallsCount = 0 + open var pinnedEventsTimelineInternalIdPrefixCallsCount: Int { + get { + if Thread.isMainThread { + return pinnedEventsTimelineInternalIdPrefixUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = pinnedEventsTimelineInternalIdPrefixUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + pinnedEventsTimelineInternalIdPrefixUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + pinnedEventsTimelineInternalIdPrefixUnderlyingCallsCount = newValue + } + } + } + } + open var pinnedEventsTimelineInternalIdPrefixCalled: Bool { + return pinnedEventsTimelineInternalIdPrefixCallsCount > 0 + } + open var pinnedEventsTimelineInternalIdPrefixReceivedInternalIdPrefix: String? + open var pinnedEventsTimelineInternalIdPrefixReceivedInvocations: [String?] = [] + + var pinnedEventsTimelineInternalIdPrefixUnderlyingReturnValue: Timeline! + open var pinnedEventsTimelineInternalIdPrefixReturnValue: Timeline! { + get { + if Thread.isMainThread { + return pinnedEventsTimelineInternalIdPrefixUnderlyingReturnValue + } else { + var returnValue: Timeline? = nil + DispatchQueue.main.sync { + returnValue = pinnedEventsTimelineInternalIdPrefixUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + pinnedEventsTimelineInternalIdPrefixUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + pinnedEventsTimelineInternalIdPrefixUnderlyingReturnValue = newValue + } + } + } + } + open var pinnedEventsTimelineInternalIdPrefixClosure: ((String?) async throws -> Timeline)? + + open override func pinnedEventsTimeline(internalIdPrefix: String?) async throws -> Timeline { + if let error = pinnedEventsTimelineInternalIdPrefixThrowableError { + throw error + } + pinnedEventsTimelineInternalIdPrefixCallsCount += 1 + pinnedEventsTimelineInternalIdPrefixReceivedInternalIdPrefix = internalIdPrefix + DispatchQueue.main.async { + self.pinnedEventsTimelineInternalIdPrefixReceivedInvocations.append(internalIdPrefix) + } + if let pinnedEventsTimelineInternalIdPrefixClosure = pinnedEventsTimelineInternalIdPrefixClosure { + return try await pinnedEventsTimelineInternalIdPrefixClosure(internalIdPrefix) + } else { + return pinnedEventsTimelineInternalIdPrefixReturnValue + } + } + //MARK: - rawName var rawNameUnderlyingCallsCount = 0 diff --git a/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift b/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift index b14a2fa362..380ef1bcdc 100644 --- a/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift +++ b/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift @@ -24,7 +24,7 @@ extension RoomTimelineControllerFactoryMock { convenience init(configuration: RoomTimelineControllerFactoryMockConfiguration) { self.init() - buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue = configuration.timelineController ?? { + buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryReturnValue = configuration.timelineController ?? { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk return timelineController diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 1e4058e6e4..44c2121297 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -23,6 +23,7 @@ struct RoomScreenCoordinatorParameters { let roomProxy: RoomProxyProtocol var focussedEventID: String? let timelineController: RoomTimelineControllerProtocol + let pinnedEventsTimelineController: RoomTimelineControllerProtocol let mediaProvider: MediaProviderProtocol let mediaPlayerProvider: MediaPlayerProviderProtocol let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol @@ -63,6 +64,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { let viewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy, focussedEventID: parameters.focussedEventID, timelineController: parameters.timelineController, + pinnedEventsTimelineController: parameters.pinnedEventsTimelineController, mediaProvider: parameters.mediaProvider, mediaPlayerProvider: parameters.mediaPlayerProvider, voiceMessageMediaManager: parameters.voiceMessageMediaManager, diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 1897d8f8d5..294986d22c 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -175,7 +175,7 @@ struct RoomScreenViewState: BindableState { var pinnedEventsState = PinnedEventsState() var shouldShowPinBanner: Bool { - isPinningEnabled && !pinnedEventsState.pinnedEventIDs.isEmpty && lastScrollDirection != .top + isPinningEnabled && !pinnedEventsState.pinnedEvents.isEmpty && lastScrollDirection != .top } var canJoinCall = false @@ -297,14 +297,14 @@ enum ScrollDirection: Equatable { struct PinnedEventsState: Equatable { // For now these will only contain and show the event IDs, but in the future they will also contain the content - var pinnedEventIDs: OrderedSet = [] { + var pinnedEvents: OrderedDictionary = [:] { didSet { - if selectedPinEventID == nil, !pinnedEventIDs.isEmpty { - selectedPinEventID = pinnedEventIDs.first - } else if pinnedEventIDs.isEmpty { + if selectedPinEventID == nil, !pinnedEvents.isEmpty { + selectedPinEventID = pinnedEvents.firstNonNil { $0.key } + } else if pinnedEvents.isEmpty { selectedPinEventID = nil - } else if let selectedPinEventID, !pinnedEventIDs.contains(selectedPinEventID) { - self.selectedPinEventID = pinnedEventIDs.first + } else if let selectedPinEventID, !pinnedEvents.keys.set.contains(selectedPinEventID) { + self.selectedPinEventID = pinnedEvents.firstNonNil { $0.key } } } } @@ -315,7 +315,7 @@ struct PinnedEventsState: Equatable { guard let selectedPinEventID else { return 0 } - return pinnedEventIDs.firstIndex(of: selectedPinEventID) ?? 0 + return pinnedEvents.keys.firstIndex(of: selectedPinEventID) ?? 0 } // For now we show the event ID as the content, but is just until we have a way to get the real content @@ -324,11 +324,11 @@ struct PinnedEventsState: Equatable { } mutating func nextPin() { - guard !pinnedEventIDs.isEmpty else { + guard !pinnedEvents.isEmpty else { return } let currentIndex = selectedPinIndex - let nextIndex = (currentIndex + 1) % pinnedEventIDs.count - selectedPinEventID = pinnedEventIDs[nextIndex] + let nextIndex = (currentIndex + 1) % pinnedEvents.count + selectedPinEventID = pinnedEvents.keys[nextIndex] } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 4d32e659a7..9f898d6fd9 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -31,6 +31,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol private let roomProxy: RoomProxyProtocol private let timelineController: RoomTimelineControllerProtocol + private let pinnedEventsTimelineController: RoomTimelineControllerProtocol private let mediaPlayerProvider: MediaPlayerProviderProtocol private let userIndicatorController: UserIndicatorControllerProtocol private let appMediator: AppMediatorProtocol @@ -52,6 +53,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol init(roomProxy: RoomProxyProtocol, focussedEventID: String? = nil, timelineController: RoomTimelineControllerProtocol, + pinnedEventsTimelineController: RoomTimelineControllerProtocol, mediaProvider: MediaProviderProtocol, mediaPlayerProvider: MediaPlayerProviderProtocol, voiceMessageMediaManager: VoiceMessageMediaManagerProtocol, @@ -60,6 +62,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol appSettings: AppSettings, analyticsService: AnalyticsService) { self.timelineController = timelineController + self.pinnedEventsTimelineController = pinnedEventsTimelineController self.mediaPlayerProvider = mediaPlayerProvider self.roomProxy = roomProxy self.appSettings = appSettings @@ -111,6 +114,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } buildTimelineViews(timelineItems: timelineController.timelineItems) + buildPinnedEventsContent(timelineItems: timelineController.timelineItems) updateMembers(roomProxy.membersPublisher.value) @@ -403,6 +407,20 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } .store(in: &cancellables) + + pinnedEventsTimelineController.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in + guard let self else { return } + + switch callback { + case .updatedTimelineItems(let updatedItems, _): + buildPinnedEventsContent(timelineItems: updatedItems) + default: + break + } + } + .store(in: &cancellables) let roomInfoSubscription = roomProxy .actionsPublisher @@ -418,20 +436,20 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } .store(in: &cancellables) - Task { [weak self] in - guard let self else { - return - } - // If the subscription has sent a value before the Task has started it might be lost, so before entering the loop we always do an update. - await state.pinnedEventsState.pinnedEventIDs = .init(roomProxy.pinnedEventIDs) - for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values { - guard !Task.isCancelled else { - return - } - await state.pinnedEventsState.pinnedEventIDs = .init(roomProxy.pinnedEventIDs) - } - } - .store(in: &cancellables) +// Task { [weak self] in +// guard let self else { +// return +// } +// // If the subscription has sent a value before the Task has started it might be lost, so before entering the loop we always do an update. +// await state.pinnedEventsState.pinnedEventIDs = .init(roomProxy.pinnedEventIDs) +// for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values { +// guard !Task.isCancelled else { +// return +// } +// await state.pinnedEventsState.pinnedEventIDs = .init(roomProxy.pinnedEventIDs) +// } +// } +// .store(in: &cancellables) appSettings.$sharePresence .weakAssign(to: \.state.showReadReceipts, on: self) @@ -635,6 +653,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol // MARK: - Timeline Item Building + private func buildPinnedEventsContent(timelineItems: [RoomTimelineItemProtocol]) { + var timelineItemsDictionary = OrderedDictionary() + + for item in timelineItems { + // Only remote events are pinned + if let eventID = item.id.eventID { + timelineItemsDictionary.updateValue(.init(item: item), forKey: eventID) + } + } + + state.pinnedEventsState.pinnedEvents = timelineItemsDictionary + } + private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) { var timelineItemsDictionary = OrderedDictionary() @@ -841,6 +872,7 @@ extension RoomScreenViewModel { static let mock = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "Preview room")), focussedEventID: nil, timelineController: MockRoomTimelineController(), + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift b/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift index 506ebdd9a3..46fa28e3d1 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift @@ -27,7 +27,7 @@ struct PinnedItemsBannerView: View { let index = pinnedEventsState.selectedPinIndex + 1 let boldPlaceholder = "{bold}" var finalString = AttributedString(L10n.screenRoomPinnedBannerIndicatorDescription(boldPlaceholder)) - var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, pinnedEventsState.pinnedEventIDs.count)) + var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, pinnedEventsState.pinnedEvents.count)) boldString.bold() finalString.replace(boldPlaceholder, with: boldString) return finalString @@ -48,7 +48,7 @@ struct PinnedItemsBannerView: View { Button { onMainButtonTap() } label: { HStack(spacing: 0) { HStack(spacing: 10) { - PinnedItemsIndicatorView(pinIndex: pinnedEventsState.selectedPinIndex, pinsCount: pinnedEventsState.pinnedEventIDs.count) + PinnedItemsIndicatorView(pinIndex: pinnedEventsState.selectedPinIndex, pinsCount: pinnedEventsState.pinnedEvents.count) .accessibilityHidden(true) CompoundIcon(\.pinSolid, size: .small, relativeTo: .compound.bodyMD) .foregroundColor(Color.compound.iconSecondaryAlpha) @@ -87,7 +87,7 @@ struct PinnedItemsBannerView: View { struct PinnedItemsBannerView_Previews: PreviewProvider, TestablePreview { static var previews: some View { - PinnedItemsBannerView(pinnedEventsState: .init(pinnedEventIDs: ["Content", "NotShown1", "NotShown2"], selectedPinEventID: "Content"), + PinnedItemsBannerView(pinnedEventsState: .init(pinnedEvents: [:], selectedPinEventID: "Content"), onMainButtonTap: { }, onViewAllButtonTap: { }) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift b/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift index 165c8aae93..6562c5d09a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift @@ -54,6 +54,7 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview { let roomProxyMock = RoomProxyMock(.init(name: "Room", members: members)) let mock = RoomScreenViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 45ece1897a..0fa424ee05 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -69,7 +69,7 @@ struct RoomScreen: View { canCurrentUserRedactSelf: context.viewState.canCurrentUserRedactSelf, canCurrentUserRedactOthers: context.viewState.canCurrentUserRedactOthers, canCurrentUserPin: context.viewState.canCurrentUserPin, - pinnedEventIDs: context.viewState.pinnedEventsState.pinnedEventIDs.set, + pinnedEventIDs: context.viewState.pinnedEventsState.pinnedEvents.keys.set, isDM: context.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: context.viewState.isViewSourceEnabled).makeActions() if let actions { @@ -209,6 +209,7 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview { name: "Preview room", hasOngoingCall: true)), timelineController: MockRoomTimelineController(), + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index 06ddbd5098..f5017313a2 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -145,7 +145,7 @@ struct TimelineItemBubbledStylerView: View { canCurrentUserRedactSelf: context.viewState.canCurrentUserRedactSelf, canCurrentUserRedactOthers: context.viewState.canCurrentUserRedactOthers, canCurrentUserPin: context.viewState.canCurrentUserPin, - pinnedEventIDs: context.viewState.pinnedEventsState.pinnedEventIDs.set, + pinnedEventIDs: context.viewState.pinnedEventsState.pinnedEvents.keys.set, isDM: context.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: context.viewState.isViewSourceEnabled) TimelineItemMacContextMenu(item: timelineItem, actionProvider: provider) { action in diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift index e4933ee342..7239ca2801 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift @@ -92,6 +92,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview { static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "Test", members: members)), timelineController: MockRoomTimelineController(), + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift index 3152077a99..d331a45eca 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift @@ -96,7 +96,7 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider { static let focussedEventID = "RoomTimelineItemFixtures.default.5" static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "Preview room")), focussedEventID: focussedEventID, - timelineController: MockRoomTimelineController(), + timelineController: MockRoomTimelineController(), pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift index 5a0b15423a..f0d585314e 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift @@ -82,6 +82,7 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview { static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(id: "stable_id", name: "Preview room")), timelineController: MockRoomTimelineController(), + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 34d435ea15..db93b7b283 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -23,6 +23,7 @@ class RoomProxy: RoomProxyProtocol { private let roomListItem: RoomListItemProtocol private let room: RoomProtocol let timeline: TimelineProxyProtocol + let pinnedEventsTimeline: TimelineProxyProtocol // periphery:ignore - required for instance retention in the rust codebase private var roomInfoObservationToken: TaskHandle? @@ -137,6 +138,7 @@ class RoomProxy: RoomProxyProtocol { do { timeline = try await TimelineProxy(timeline: room.timeline(), isLive: true) + pinnedEventsTimeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: "pinned_events_timeline"), isLive: false) } catch { MXLog.error("Failed creating timeline with error: \(error)") return nil @@ -160,6 +162,7 @@ class RoomProxy: RoomProxyProtocol { roomListItem.subscribe(settings: settings) await timeline.subscribeForUpdates() + await pinnedEventsTimeline.subscribeForUpdates() subscribeToRoomInfoUpdates() diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index ec161ed683..90776074fd 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -66,6 +66,8 @@ protocol RoomProxyProtocol { var timeline: TimelineProxyProtocol { get } + var pinnedEventsTimeline: TimelineProxyProtocol { get } + func subscribeForUpdates() async func subscribeToRoomInfoUpdates() diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index 8db6d0c3bd..ad727a623f 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -42,16 +42,17 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } init(roomProxy: RoomProxyProtocol, + timelineProxy: TimelineProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol, appSettings: AppSettings) { self.roomProxy = roomProxy - liveTimelineProvider = roomProxy.timeline.timelineProvider + liveTimelineProvider = timelineProxy.timelineProvider self.timelineItemFactory = timelineItemFactory self.appSettings = appSettings serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility) - activeTimeline = roomProxy.timeline + activeTimeline = timelineProxy activeTimelineProvider = liveTimelineProvider NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil) diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift index 2dbfff198c..67aa22ad1a 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift @@ -18,9 +18,11 @@ import Foundation struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol { func buildRoomTimelineController(roomProxy: RoomProxyProtocol, + timelineProxy: TimelineProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol { RoomTimelineController(roomProxy: roomProxy, + timelineProxy: timelineProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory, appSettings: ServiceLocator.shared.settings) diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift index 3a5d9d5512..e3f534c32d 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift @@ -19,6 +19,7 @@ import Foundation @MainActor protocol RoomTimelineControllerFactoryProtocol { func buildRoomTimelineController(roomProxy: RoomProxyProtocol, + timelineProxy: TimelineProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index e138a50843..cf20112eb4 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -250,6 +250,7 @@ class MockScreen: Identifiable { let navigationStackCoordinator = NavigationStackCoordinator() let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Some room name", avatarURL: nil)), timelineController: MockRoomTimelineController(), + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -267,6 +268,7 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -284,6 +286,7 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.default let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -301,6 +304,7 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.smallChunkWithReadReceipts let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -321,6 +325,7 @@ class MockScreen: Identifiable { timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Small timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -341,6 +346,7 @@ class MockScreen: Identifiable { timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Small timeline, paginating", avatarURL: URL.picturesDirectory)), timelineController: timelineController, + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -361,6 +367,7 @@ class MockScreen: Identifiable { timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -382,6 +389,7 @@ class MockScreen: Identifiable { timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -402,6 +410,7 @@ class MockScreen: Identifiable { timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -421,6 +430,7 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.permalinkChunk let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Timeline highlight", avatarURL: URL.picturesDirectory)), timelineController: timelineController, + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -454,6 +464,7 @@ class MockScreen: Identifiable { timelineController.incomingItems = [] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -474,6 +485,7 @@ class MockScreen: Identifiable { timelineController.incomingItems = [] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -494,6 +506,7 @@ class MockScreen: Identifiable { timelineController.incomingItems = [] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, + pinnedEventsTimelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -634,6 +647,7 @@ class MockScreen: Identifiable { ServiceLocator.shared.settings.migratedAccounts[clientProxy.userID] = true let timelineController = RoomTimelineController(roomProxy: roomProxy, + timelineProxy: roomProxy.timeline, initialFocussedEventID: nil, timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org", encryptionAuthenticityEnabled: true, From 7bcd26339033e286f714123e90fb618fbfb4ca01 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Tue, 30 Jul 2024 17:11:43 +0200 Subject: [PATCH 2/8] better solution that loads the timeline async --- .../RoomFlowCoordinator.swift | 29 +++- .../Mocks/Generated/GeneratedMocks.swift | 144 ++++++++++++++---- ElementX/Sources/Mocks/RoomProxyMock.swift | 2 +- .../RoomTimelineControllerFactoryMock.swift | 4 +- .../RoomScreen/RoomScreenCoordinator.swift | 4 +- .../Screens/RoomScreen/RoomScreenModels.swift | 8 +- .../RoomScreen/RoomScreenViewModel.swift | 73 +++++---- .../ReadReceiptsSummaryView.swift | 2 +- .../Screens/RoomScreen/View/RoomScreen.swift | 4 +- .../Style/TimelineItemBubbledStylerView.swift | 2 +- .../TimelineReadReceiptsView.swift | 2 +- .../HighlightedTimelineItemModifier.swift | 3 +- .../View/Timeline/TimelineView.swift | 2 +- .../Sources/Services/Room/RoomProxy.swift | 28 +++- .../Services/Room/RoomProxyProtocol.swift | 4 +- .../RoomTimelineControllerFactory.swift | 11 +- ...oomTimelineControllerFactoryProtocol.swift | 4 +- .../UITests/UITestsAppCoordinator.swift | 26 ++-- 18 files changed, 245 insertions(+), 107 deletions(-) diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index ef8dc56566..4ddad2c2cd 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -83,7 +83,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } private var timelineController: RoomTimelineControllerProtocol? - private var pinnedItemsTimelineController: RoomTimelineControllerProtocol? init(roomID: String, userSession: UserSessionProtocol, @@ -546,14 +545,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy, - timelineProxy: roomProxy.timeline, initialFocussedEventID: focussedEventID, timelineItemFactory: timelineItemFactory) self.timelineController = timelineController - let pinnedItemsTimelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy, timelineProxy: roomProxy.pinnedEventsTimeline, initialFocussedEventID: nil, timelineItemFactory: timelineItemFactory) - self.pinnedItemsTimelineController = pinnedItemsTimelineController - analytics.trackViewRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace) let completionSuggestionService = CompletionSuggestionService(roomProxy: roomProxy) @@ -563,7 +558,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy, focussedEventID: focussedEventID, timelineController: timelineController, - pinnedEventsTimelineController: pinnedItemsTimelineController, + pinnedTimelineBuilder: PinnedEventsTimelineBuilder(timelineFactory: roomTimelineControllerFactory, timelineItemsFactory: timelineItemFactory), mediaProvider: userSession.mediaProvider, mediaPlayerProvider: MediaPlayerProvider(), voiceMessageMediaManager: userSession.voiceMessageMediaManager, @@ -1045,7 +1040,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) let roomTimelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy, - timelineProxy: roomProxy.timeline, initialFocussedEventID: nil, timelineItemFactory: timelineItemFactory) @@ -1454,3 +1448,24 @@ private extension Result { } } } + +struct PinnedEventsTimelineBuilder { + private let timelineFactory: RoomTimelineControllerFactoryProtocol + private let timelineItemsFactory: RoomTimelineItemFactoryProtocol + + init(timelineFactory: RoomTimelineControllerFactoryProtocol, + timelineItemsFactory: RoomTimelineItemFactoryProtocol) { + self.timelineFactory = timelineFactory + self.timelineItemsFactory = timelineItemsFactory + } + + func buildPinnedEventsTimelineController(roomProxy: RoomProxyProtocol) async -> RoomTimelineControllerProtocol? { + await timelineFactory.buildPinnedEventsTimelineController(roomProxy: roomProxy, timelineItemFactory: timelineItemsFactory) + } + + @MainActor + static func mock() -> PinnedEventsTimelineBuilder { + PinnedEventsTimelineBuilder(timelineFactory: RoomTimelineControllerFactoryMock(configuration: .init()), + timelineItemsFactory: RoomTimelineItemFactory(userID: UUID().uuidString, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: UUID().uuidString))) + } +} diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 55482daf57..662d967171 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -8320,7 +8320,7 @@ class RoomProxyMock: RoomProxyProtocol { return pinnedEventIDsCallsCount > 0 } - var pinnedEventIDs: [String] { + var pinnedEventIDs: Set { get async { pinnedEventIDsCallsCount += 1 if let pinnedEventIDsClosure = pinnedEventIDsClosure { @@ -8330,8 +8330,8 @@ class RoomProxyMock: RoomProxyProtocol { } } } - var underlyingPinnedEventIDs: [String]! - var pinnedEventIDsClosure: (() async -> [String])? + var underlyingPinnedEventIDs: Set! + var pinnedEventIDsClosure: (() async -> Set)? var membership: Membership { get { return underlyingMembership } set(value) { underlyingMembership = value } @@ -8403,11 +8403,23 @@ class RoomProxyMock: RoomProxyProtocol { set(value) { underlyingTimeline = value } } var underlyingTimeline: TimelineProxyProtocol! - var pinnedEventsTimeline: TimelineProxyProtocol { - get { return underlyingPinnedEventsTimeline } - set(value) { underlyingPinnedEventsTimeline = value } + var pinnedEventsTimelineCallsCount = 0 + var pinnedEventsTimelineCalled: Bool { + return pinnedEventsTimelineCallsCount > 0 } - var underlyingPinnedEventsTimeline: TimelineProxyProtocol! + + var pinnedEventsTimeline: TimelineProxyProtocol? { + get async { + pinnedEventsTimelineCallsCount += 1 + if let pinnedEventsTimelineClosure = pinnedEventsTimelineClosure { + return await pinnedEventsTimelineClosure() + } else { + return underlyingPinnedEventsTimeline + } + } + } + var underlyingPinnedEventsTimeline: TimelineProxyProtocol? + var pinnedEventsTimelineClosure: (() async -> TimelineProxyProtocol?)? //MARK: - subscribeForUpdates @@ -11466,15 +11478,15 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { //MARK: - buildRoomTimelineController - var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = 0 - var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryCallsCount: Int { + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = 0 + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount: Int { get { if Thread.isMainThread { - return buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount + returnValue = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount } return returnValue! @@ -11482,29 +11494,99 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { } set { if Thread.isMainThread { - buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = newValue + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = newValue + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = newValue } } } } - var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryCalled: Bool { - return buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryCallsCount > 0 + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCalled: Bool { + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount > 0 } - var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments: (roomProxy: RoomProxyProtocol, timelineProxy: TimelineProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol)? - var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryReceivedInvocations: [(roomProxy: RoomProxyProtocol, timelineProxy: TimelineProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol)] = [] + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments: (roomProxy: RoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol)? + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedInvocations: [(roomProxy: RoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol)] = [] - var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue: RoomTimelineControllerProtocol! - var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryReturnValue: RoomTimelineControllerProtocol! { + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue: RoomTimelineControllerProtocol! + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue: RoomTimelineControllerProtocol! { get { if Thread.isMainThread { - return buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue } else { var returnValue: RoomTimelineControllerProtocol? = nil DispatchQueue.main.sync { - returnValue = buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue + returnValue = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue = newValue + } + } + } + } + var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure: ((RoomProxyProtocol, String?, RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol)? + + func buildRoomTimelineController(roomProxy: RoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol { + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount += 1 + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments = (roomProxy: roomProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory) + DispatchQueue.main.async { + self.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedInvocations.append((roomProxy: roomProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory)) + } + if let buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure { + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure(roomProxy, initialFocussedEventID, timelineItemFactory) + } else { + return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue + } + } + //MARK: - buildPinnedEventsTimelineController + + var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = 0 + var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryCallsCount: Int { + get { + if Thread.isMainThread { + return buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = newValue + } + } + } + } + var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryCalled: Bool { + return buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryCallsCount > 0 + } + var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryReceivedArguments: (roomProxy: RoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol)? + var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryReceivedInvocations: [(roomProxy: RoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol)] = [] + + var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue: RoomTimelineControllerProtocol? + var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryReturnValue: RoomTimelineControllerProtocol? { + get { + if Thread.isMainThread { + return buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue + } else { + var returnValue: RoomTimelineControllerProtocol?? = nil + DispatchQueue.main.sync { + returnValue = buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue } return returnValue! @@ -11512,26 +11594,26 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { } set { if Thread.isMainThread { - buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue = newValue + buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue = newValue + buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue = newValue } } } } - var buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryClosure: ((RoomProxyProtocol, TimelineProxyProtocol, String?, RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol)? + var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryClosure: ((RoomProxyProtocol, RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol?)? - func buildRoomTimelineController(roomProxy: RoomProxyProtocol, timelineProxy: TimelineProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol { - buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryCallsCount += 1 - buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments = (roomProxy: roomProxy, timelineProxy: timelineProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory) + func buildPinnedEventsTimelineController(roomProxy: RoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? { + buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryCallsCount += 1 + buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryReceivedArguments = (roomProxy: roomProxy, timelineItemFactory: timelineItemFactory) DispatchQueue.main.async { - self.buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryReceivedInvocations.append((roomProxy: roomProxy, timelineProxy: timelineProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory)) + self.buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryReceivedInvocations.append((roomProxy: roomProxy, timelineItemFactory: timelineItemFactory)) } - if let buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryClosure = buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryClosure { - return buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryClosure(roomProxy, timelineProxy, initialFocussedEventID, timelineItemFactory) + if let buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryClosure = buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryClosure { + return await buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryClosure(roomProxy, timelineItemFactory) } else { - return buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryReturnValue + return buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryReturnValue } } } diff --git a/ElementX/Sources/Mocks/RoomProxyMock.swift b/ElementX/Sources/Mocks/RoomProxyMock.swift index decb0df185..b82e7daa20 100644 --- a/ElementX/Sources/Mocks/RoomProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomProxyMock.swift @@ -29,7 +29,7 @@ struct RoomProxyMockConfiguration { var isEncrypted = true var hasOngoingCall = true var canonicalAlias: String? - var pinnedEventIDs: [String] = [] + var pinnedEventIDs: Set = [] var timelineStartReached = false diff --git a/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift b/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift index 380ef1bcdc..a3e6638f6c 100644 --- a/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift +++ b/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift @@ -24,10 +24,12 @@ extension RoomTimelineControllerFactoryMock { convenience init(configuration: RoomTimelineControllerFactoryMockConfiguration) { self.init() - buildRoomTimelineControllerRoomProxyTimelineProxyInitialFocussedEventIDTimelineItemFactoryReturnValue = configuration.timelineController ?? { + buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue = configuration.timelineController ?? { let timelineController = MockRoomTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk return timelineController }() + + buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryReturnValue = nil } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 44c2121297..45ee1540af 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -23,7 +23,7 @@ struct RoomScreenCoordinatorParameters { let roomProxy: RoomProxyProtocol var focussedEventID: String? let timelineController: RoomTimelineControllerProtocol - let pinnedEventsTimelineController: RoomTimelineControllerProtocol + let pinnedTimelineBuilder: PinnedEventsTimelineBuilder let mediaProvider: MediaProviderProtocol let mediaPlayerProvider: MediaPlayerProviderProtocol let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol @@ -64,7 +64,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { let viewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy, focussedEventID: parameters.focussedEventID, timelineController: parameters.timelineController, - pinnedEventsTimelineController: parameters.pinnedEventsTimelineController, + pinnedTimelineBuilder: parameters.pinnedTimelineBuilder, mediaProvider: parameters.mediaProvider, mediaPlayerProvider: parameters.mediaPlayerProvider, voiceMessageMediaManager: parameters.voiceMessageMediaManager, diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 294986d22c..0b7e413e12 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -172,6 +172,9 @@ struct RoomScreenViewState: BindableState { var isPinningEnabled = false var lastScrollDirection: ScrollDirection? + // The `pinnedEventIDs` is used only to determine if an item can be pinned or not since it does not depend on the pinned events timeline, but on the room info update which is faster + var pinnedEventIDs: Set = [] + // This is used to controler the banner var pinnedEventsState = PinnedEventsState() var shouldShowPinBanner: Bool { @@ -312,10 +315,11 @@ struct PinnedEventsState: Equatable { var selectedPinEventID: String? var selectedPinIndex: Int { + let defaultValue = pinnedEvents.isEmpty ? 0 : pinnedEvents.count - 1 guard let selectedPinEventID else { - return 0 + return defaultValue } - return pinnedEvents.keys.firstIndex(of: selectedPinEventID) ?? 0 + return pinnedEvents.keys.firstIndex(of: selectedPinEventID) ?? defaultValue } // For now we show the event ID as the content, but is just until we have a way to get the real content diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 9f898d6fd9..cda0edd0e0 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -31,7 +31,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol private let roomProxy: RoomProxyProtocol private let timelineController: RoomTimelineControllerProtocol - private let pinnedEventsTimelineController: RoomTimelineControllerProtocol private let mediaPlayerProvider: MediaPlayerProviderProtocol private let userIndicatorController: UserIndicatorControllerProtocol private let appMediator: AppMediatorProtocol @@ -49,11 +48,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol private var paginateBackwardsTask: Task? private var paginateForwardsTask: Task? + + private var pinnedEventsTimelineController: RoomTimelineControllerProtocol? init(roomProxy: RoomProxyProtocol, focussedEventID: String? = nil, timelineController: RoomTimelineControllerProtocol, - pinnedEventsTimelineController: RoomTimelineControllerProtocol, + pinnedTimelineBuilder: PinnedEventsTimelineBuilder, mediaProvider: MediaProviderProtocol, mediaPlayerProvider: MediaPlayerProviderProtocol, voiceMessageMediaManager: VoiceMessageMediaManagerProtocol, @@ -62,7 +63,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol appSettings: AppSettings, analyticsService: AnalyticsService) { self.timelineController = timelineController - self.pinnedEventsTimelineController = pinnedEventsTimelineController self.mediaPlayerProvider = mediaPlayerProvider self.roomProxy = roomProxy self.appSettings = appSettings @@ -114,7 +114,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } buildTimelineViews(timelineItems: timelineController.timelineItems) - buildPinnedEventsContent(timelineItems: timelineController.timelineItems) updateMembers(roomProxy.membersPublisher.value) @@ -128,6 +127,28 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.canJoinCall = permission } } + + Task { + guard let pinnedEventsTimelineController = await pinnedTimelineBuilder.buildPinnedEventsTimelineController(roomProxy: roomProxy) else { + return + } + + buildPinnedEventsContent(timelineItems: pinnedEventsTimelineController.timelineItems) + + pinnedEventsTimelineController.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in + guard let self else { return } + + switch callback { + case .updatedTimelineItems(let updatedItems, _): + buildPinnedEventsContent(timelineItems: updatedItems) + default: + break + } + } + .store(in: &cancellables) + } } // MARK: - Public @@ -407,20 +428,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } .store(in: &cancellables) - - pinnedEventsTimelineController.callbacks - .receive(on: DispatchQueue.main) - .sink { [weak self] callback in - guard let self else { return } - - switch callback { - case .updatedTimelineItems(let updatedItems, _): - buildPinnedEventsContent(timelineItems: updatedItems) - default: - break - } - } - .store(in: &cancellables) let roomInfoSubscription = roomProxy .actionsPublisher @@ -436,20 +443,20 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } .store(in: &cancellables) -// Task { [weak self] in -// guard let self else { -// return -// } -// // If the subscription has sent a value before the Task has started it might be lost, so before entering the loop we always do an update. -// await state.pinnedEventsState.pinnedEventIDs = .init(roomProxy.pinnedEventIDs) -// for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values { -// guard !Task.isCancelled else { -// return -// } -// await state.pinnedEventsState.pinnedEventIDs = .init(roomProxy.pinnedEventIDs) -// } -// } -// .store(in: &cancellables) + Task { [weak self] in + guard let self else { + return + } + // If the subscription has sent a value before the Task has started it might be lost, so before entering the loop we always do an update. + await state.pinnedEventIDs = roomProxy.pinnedEventIDs + for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values { + guard !Task.isCancelled else { + return + } + await state.pinnedEventIDs = roomProxy.pinnedEventIDs + } + } + .store(in: &cancellables) appSettings.$sharePresence .weakAssign(to: \.state.showReadReceipts, on: self) @@ -872,7 +879,7 @@ extension RoomScreenViewModel { static let mock = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "Preview room")), focussedEventID: nil, timelineController: MockRoomTimelineController(), - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift b/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift index 6562c5d09a..6efc932363 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift @@ -54,7 +54,7 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview { let roomProxyMock = RoomProxyMock(.init(name: "Room", members: members)) let mock = RoomScreenViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 0fa424ee05..66e15cbdfa 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -69,7 +69,7 @@ struct RoomScreen: View { canCurrentUserRedactSelf: context.viewState.canCurrentUserRedactSelf, canCurrentUserRedactOthers: context.viewState.canCurrentUserRedactOthers, canCurrentUserPin: context.viewState.canCurrentUserPin, - pinnedEventIDs: context.viewState.pinnedEventsState.pinnedEvents.keys.set, + pinnedEventIDs: context.viewState.pinnedEventIDs, isDM: context.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: context.viewState.isViewSourceEnabled).makeActions() if let actions { @@ -209,7 +209,7 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview { name: "Preview room", hasOngoingCall: true)), timelineController: MockRoomTimelineController(), - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index f5017313a2..cf5ddb862c 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -145,7 +145,7 @@ struct TimelineItemBubbledStylerView: View { canCurrentUserRedactSelf: context.viewState.canCurrentUserRedactSelf, canCurrentUserRedactOthers: context.viewState.canCurrentUserRedactOthers, canCurrentUserPin: context.viewState.canCurrentUserPin, - pinnedEventIDs: context.viewState.pinnedEventsState.pinnedEvents.keys.set, + pinnedEventIDs: context.viewState.pinnedEventIDs, isDM: context.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: context.viewState.isViewSourceEnabled) TimelineItemMacContextMenu(item: timelineItem, actionProvider: provider) { action in diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift index 7239ca2801..81d37ab08f 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift @@ -92,7 +92,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview { static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "Test", members: members)), timelineController: MockRoomTimelineController(), - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift index d331a45eca..57187e2c5a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift @@ -96,7 +96,8 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider { static let focussedEventID = "RoomTimelineItemFixtures.default.5" static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "Preview room")), focussedEventID: focussedEventID, - timelineController: MockRoomTimelineController(), pinnedEventsTimelineController: MockRoomTimelineController(), + timelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift index f0d585314e..7e4a3f3dc2 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift @@ -82,7 +82,7 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview { static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(id: "stable_id", name: "Preview room")), timelineController: MockRoomTimelineController(), - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index db93b7b283..dea915945e 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -23,7 +23,24 @@ class RoomProxy: RoomProxyProtocol { private let roomListItem: RoomListItemProtocol private let room: RoomProtocol let timeline: TimelineProxyProtocol - let pinnedEventsTimeline: TimelineProxyProtocol + private var innerPinnedEventsTimeline: TimelineProxyProtocol? + var pinnedEventsTimeline: TimelineProxyProtocol? { + get async { + if let innerPinnedEventsTimeline { + return innerPinnedEventsTimeline + } else { + do { + let timeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: nil), isLive: false) + await timeline.subscribeForUpdates() + innerPinnedEventsTimeline = timeline + return timeline + } catch { + MXLog.error("Failed creating pinned events timeline with error: \(error)") + return nil + } + } + } + } // periphery:ignore - required for instance retention in the rust codebase private var roomInfoObservationToken: TaskHandle? @@ -93,9 +110,12 @@ class RoomProxy: RoomProxyProtocol { } } - var pinnedEventIDs: [String] { + var pinnedEventIDs: Set { get async { - await (try? room.roomInfo().pinnedEventIds) ?? [] + guard let pinnedEventIDs = try? await room.roomInfo().pinnedEventIds else { + return [] + } + return .init(pinnedEventIDs) } } @@ -138,7 +158,6 @@ class RoomProxy: RoomProxyProtocol { do { timeline = try await TimelineProxy(timeline: room.timeline(), isLive: true) - pinnedEventsTimeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: "pinned_events_timeline"), isLive: false) } catch { MXLog.error("Failed creating timeline with error: \(error)") return nil @@ -162,7 +181,6 @@ class RoomProxy: RoomProxyProtocol { roomListItem.subscribe(settings: settings) await timeline.subscribeForUpdates() - await pinnedEventsTimeline.subscribeForUpdates() subscribeToRoomInfoUpdates() diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index 90776074fd..df42c92415 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -38,7 +38,7 @@ protocol RoomProxyProtocol { var isSpace: Bool { get } var isEncrypted: Bool { get } var isFavourite: Bool { get async } - var pinnedEventIDs: [String] { get async } + var pinnedEventIDs: Set { get async } var membership: Membership { get } var inviter: RoomMemberProxyProtocol? { get async } var hasOngoingCall: Bool { get } @@ -66,7 +66,7 @@ protocol RoomProxyProtocol { var timeline: TimelineProxyProtocol { get } - var pinnedEventsTimeline: TimelineProxyProtocol { get } + var pinnedEventsTimeline: TimelineProxyProtocol? { get async } func subscribeForUpdates() async diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift index 67aa22ad1a..582e43e2ce 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift @@ -18,13 +18,20 @@ import Foundation struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol { func buildRoomTimelineController(roomProxy: RoomProxyProtocol, - timelineProxy: TimelineProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol { RoomTimelineController(roomProxy: roomProxy, - timelineProxy: timelineProxy, + timelineProxy: roomProxy.timeline, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory, appSettings: ServiceLocator.shared.settings) } + + func buildPinnedEventsTimelineController(roomProxy: RoomProxyProtocol, + timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? { + guard let pinnedEventsTimeline = await roomProxy.pinnedEventsTimeline else { + return nil + } + return RoomTimelineController(roomProxy: roomProxy, timelineProxy: pinnedEventsTimeline, initialFocussedEventID: nil, timelineItemFactory: timelineItemFactory, appSettings: ServiceLocator.shared.settings) + } } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift index e3f534c32d..e7e2469761 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift @@ -19,9 +19,11 @@ import Foundation @MainActor protocol RoomTimelineControllerFactoryProtocol { func buildRoomTimelineController(roomProxy: RoomProxyProtocol, - timelineProxy: TimelineProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol + + func buildPinnedEventsTimelineController(roomProxy: RoomProxyProtocol, + timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? } // sourcery: AutoMockable diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index cf20112eb4..145a58b967 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -250,7 +250,7 @@ class MockScreen: Identifiable { let navigationStackCoordinator = NavigationStackCoordinator() let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Some room name", avatarURL: nil)), timelineController: MockRoomTimelineController(), - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -268,7 +268,7 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -286,7 +286,7 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.default let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -304,7 +304,7 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.smallChunkWithReadReceipts let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -325,7 +325,7 @@ class MockScreen: Identifiable { timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Small timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -346,7 +346,7 @@ class MockScreen: Identifiable { timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Small timeline, paginating", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -367,7 +367,7 @@ class MockScreen: Identifiable { timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -389,7 +389,7 @@ class MockScreen: Identifiable { timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -410,7 +410,7 @@ class MockScreen: Identifiable { timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -430,7 +430,7 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.permalinkChunk let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Timeline highlight", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -464,7 +464,7 @@ class MockScreen: Identifiable { timelineController.incomingItems = [] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -485,7 +485,7 @@ class MockScreen: Identifiable { timelineController.incomingItems = [] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -506,7 +506,7 @@ class MockScreen: Identifiable { timelineController.incomingItems = [] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedEventsTimelineController: MockRoomTimelineController(), + pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), From 8b3731b1c8d73007a9ee40173a5d02fc100573a6 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Tue, 30 Jul 2024 20:19:03 +0200 Subject: [PATCH 3/8] the banner now works properly! --- .../RoomFlowCoordinator.swift | 22 ------ .../Mocks/Generated/GeneratedMocks.swift | 70 ------------------- .../RoomTimelineControllerFactoryMock.swift | 2 - .../Other/Extensions/AttributedString.swift | 5 ++ .../RoomScreen/RoomScreenCoordinator.swift | 2 - .../Screens/RoomScreen/RoomScreenModels.swift | 31 ++++---- .../RoomScreen/RoomScreenViewModel.swift | 37 +++++----- .../PinnedItemsBannerView.swift | 9 ++- .../ReadReceiptsSummaryView.swift | 1 - .../Screens/RoomScreen/View/RoomScreen.swift | 1 - .../TimelineReadReceiptsView.swift | 1 - .../HighlightedTimelineItemModifier.swift | 1 - .../View/Timeline/TimelineView.swift | 1 - .../Sources/Services/Client/ClientProxy.swift | 5 +- .../RoomSummary/RoomEventStringBuilder.swift | 15 +++- .../RoomMessageEventStringBuilder.swift | 53 +++++++++----- .../RoomTimelineControllerFactory.swift | 8 --- ...oomTimelineControllerFactoryProtocol.swift | 3 - .../UITests/UITestsAppCoordinator.swift | 13 ---- NSE/Sources/NotificationContentBuilder.swift | 2 +- .../NotificationServiceExtension.swift | 2 +- 21 files changed, 99 insertions(+), 185 deletions(-) diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 4ddad2c2cd..81619663ca 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -558,7 +558,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy, focussedEventID: focussedEventID, timelineController: timelineController, - pinnedTimelineBuilder: PinnedEventsTimelineBuilder(timelineFactory: roomTimelineControllerFactory, timelineItemsFactory: timelineItemFactory), mediaProvider: userSession.mediaProvider, mediaPlayerProvider: MediaPlayerProvider(), voiceMessageMediaManager: userSession.voiceMessageMediaManager, @@ -1448,24 +1447,3 @@ private extension Result { } } } - -struct PinnedEventsTimelineBuilder { - private let timelineFactory: RoomTimelineControllerFactoryProtocol - private let timelineItemsFactory: RoomTimelineItemFactoryProtocol - - init(timelineFactory: RoomTimelineControllerFactoryProtocol, - timelineItemsFactory: RoomTimelineItemFactoryProtocol) { - self.timelineFactory = timelineFactory - self.timelineItemsFactory = timelineItemsFactory - } - - func buildPinnedEventsTimelineController(roomProxy: RoomProxyProtocol) async -> RoomTimelineControllerProtocol? { - await timelineFactory.buildPinnedEventsTimelineController(roomProxy: roomProxy, timelineItemFactory: timelineItemsFactory) - } - - @MainActor - static func mock() -> PinnedEventsTimelineBuilder { - PinnedEventsTimelineBuilder(timelineFactory: RoomTimelineControllerFactoryMock(configuration: .init()), - timelineItemsFactory: RoomTimelineItemFactory(userID: UUID().uuidString, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: UUID().uuidString))) - } -} diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 662d967171..a53fe7e2f9 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -11546,76 +11546,6 @@ class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol { return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue } } - //MARK: - buildPinnedEventsTimelineController - - var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = 0 - var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryCallsCount: Int { - get { - if Thread.isMainThread { - return buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingCallsCount = newValue - } - } - } - } - var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryCalled: Bool { - return buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryCallsCount > 0 - } - var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryReceivedArguments: (roomProxy: RoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol)? - var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryReceivedInvocations: [(roomProxy: RoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol)] = [] - - var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue: RoomTimelineControllerProtocol? - var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryReturnValue: RoomTimelineControllerProtocol? { - get { - if Thread.isMainThread { - return buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue - } else { - var returnValue: RoomTimelineControllerProtocol?? = nil - DispatchQueue.main.sync { - returnValue = buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue = newValue - } else { - DispatchQueue.main.sync { - buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryUnderlyingReturnValue = newValue - } - } - } - } - var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryClosure: ((RoomProxyProtocol, RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol?)? - - func buildPinnedEventsTimelineController(roomProxy: RoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? { - buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryCallsCount += 1 - buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryReceivedArguments = (roomProxy: roomProxy, timelineItemFactory: timelineItemFactory) - DispatchQueue.main.async { - self.buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryReceivedInvocations.append((roomProxy: roomProxy, timelineItemFactory: timelineItemFactory)) - } - if let buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryClosure = buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryClosure { - return await buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryClosure(roomProxy, timelineItemFactory) - } else { - return buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryReturnValue - } - } } class RoomTimelineProviderMock: RoomTimelineProviderProtocol { var updatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never> { diff --git a/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift b/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift index a3e6638f6c..b14a2fa362 100644 --- a/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift +++ b/ElementX/Sources/Mocks/RoomTimelineControllerFactoryMock.swift @@ -29,7 +29,5 @@ extension RoomTimelineControllerFactoryMock { timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk return timelineController }() - - buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryReturnValue = nil } } diff --git a/ElementX/Sources/Other/Extensions/AttributedString.swift b/ElementX/Sources/Other/Extensions/AttributedString.swift index efd51b9e2e..dbe7fd7427 100644 --- a/ElementX/Sources/Other/Extensions/AttributedString.swift +++ b/ElementX/Sources/Other/Extensions/AttributedString.swift @@ -17,6 +17,11 @@ import Foundation extension AttributedString { + // faster than doing `String(characters)`: https://forums.swift.org/t/attributedstring-to-string/61667 + var string: String { + String(characters[...]) + } + var formattedComponents: [AttributedStringBuilderComponent] { runs[\.blockquote].map { value, range in var attributedString = AttributedString(self[range]) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 45ee1540af..1e4058e6e4 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -23,7 +23,6 @@ struct RoomScreenCoordinatorParameters { let roomProxy: RoomProxyProtocol var focussedEventID: String? let timelineController: RoomTimelineControllerProtocol - let pinnedTimelineBuilder: PinnedEventsTimelineBuilder let mediaProvider: MediaProviderProtocol let mediaPlayerProvider: MediaPlayerProviderProtocol let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol @@ -64,7 +63,6 @@ final class RoomScreenCoordinator: CoordinatorProtocol { let viewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy, focussedEventID: parameters.focussedEventID, timelineController: parameters.timelineController, - pinnedTimelineBuilder: parameters.pinnedTimelineBuilder, mediaProvider: parameters.mediaProvider, mediaPlayerProvider: parameters.mediaPlayerProvider, voiceMessageMediaManager: parameters.voiceMessageMediaManager, diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 0b7e413e12..007083bc5e 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -178,7 +178,7 @@ struct RoomScreenViewState: BindableState { var pinnedEventsState = PinnedEventsState() var shouldShowPinBanner: Bool { - isPinningEnabled && !pinnedEventsState.pinnedEvents.isEmpty && lastScrollDirection != .top + isPinningEnabled && !pinnedEventsState.pinnedEventsContent.isEmpty && lastScrollDirection != .top } var canJoinCall = false @@ -300,39 +300,42 @@ enum ScrollDirection: Equatable { struct PinnedEventsState: Equatable { // For now these will only contain and show the event IDs, but in the future they will also contain the content - var pinnedEvents: OrderedDictionary = [:] { + var pinnedEventsContent: OrderedDictionary = [:] { didSet { - if selectedPinEventID == nil, !pinnedEvents.isEmpty { - selectedPinEventID = pinnedEvents.firstNonNil { $0.key } - } else if pinnedEvents.isEmpty { + if selectedPinEventID == nil, !pinnedEventsContent.keys.isEmpty { + selectedPinEventID = pinnedEventsContent.keys.last + } else if pinnedEventsContent.isEmpty { selectedPinEventID = nil - } else if let selectedPinEventID, !pinnedEvents.keys.set.contains(selectedPinEventID) { - self.selectedPinEventID = pinnedEvents.firstNonNil { $0.key } + } else if let selectedPinEventID, !pinnedEventsContent.keys.set.contains(selectedPinEventID) { + self.selectedPinEventID = pinnedEventsContent.firstNonNil { $0.key } } } } - var selectedPinEventID: String? + private(set) var selectedPinEventID: String? var selectedPinIndex: Int { - let defaultValue = pinnedEvents.isEmpty ? 0 : pinnedEvents.count - 1 + let defaultValue = pinnedEventsContent.isEmpty ? 0 : pinnedEventsContent.count - 1 guard let selectedPinEventID else { return defaultValue } - return pinnedEvents.keys.firstIndex(of: selectedPinEventID) ?? defaultValue + return pinnedEventsContent.keys.firstIndex(of: selectedPinEventID) ?? defaultValue } // For now we show the event ID as the content, but is just until we have a way to get the real content var selectedPinContent: AttributedString { - .init(selectedPinEventID ?? "") + guard let selectedPinEventID else { + return AttributedString() + } + return pinnedEventsContent[selectedPinEventID] ?? AttributedString() } mutating func nextPin() { - guard !pinnedEvents.isEmpty else { + guard !pinnedEventsContent.isEmpty else { return } let currentIndex = selectedPinIndex - let nextIndex = (currentIndex + 1) % pinnedEvents.count - selectedPinEventID = pinnedEvents.keys[nextIndex] + let nextIndex = (currentIndex + 1) % pinnedEventsContent.count + selectedPinEventID = pinnedEventsContent.keys[nextIndex] } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index cda0edd0e0..88f97d3ec9 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -36,6 +36,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol private let appMediator: AppMediatorProtocol private let appSettings: AppSettings private let analyticsService: AnalyticsService + private let pinnedEventStringBuilder: RoomEventStringBuilder private let roomScreenInteractionHandler: RoomScreenInteractionHandler @@ -49,12 +50,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol private var paginateBackwardsTask: Task? private var paginateForwardsTask: Task? + // Needs to be stored to allow the provider to keep sending updates. private var pinnedEventsTimelineController: RoomTimelineControllerProtocol? init(roomProxy: RoomProxyProtocol, focussedEventID: String? = nil, timelineController: RoomTimelineControllerProtocol, - pinnedTimelineBuilder: PinnedEventsTimelineBuilder, mediaProvider: MediaProviderProtocol, mediaPlayerProvider: MediaPlayerProviderProtocol, voiceMessageMediaManager: VoiceMessageMediaManagerProtocol, @@ -69,6 +70,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol self.analyticsService = analyticsService self.userIndicatorController = userIndicatorController self.appMediator = appMediator + pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID) let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider) @@ -129,25 +131,21 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } Task { - guard let pinnedEventsTimelineController = await pinnedTimelineBuilder.buildPinnedEventsTimelineController(roomProxy: roomProxy) else { + guard let pinnedEventsTimelineProvider = await roomProxy.pinnedEventsTimeline?.timelineProvider else { return } - buildPinnedEventsContent(timelineItems: pinnedEventsTimelineController.timelineItems) + buildPinnedEventsContent(timelineItems: pinnedEventsTimelineProvider.itemProxies) - pinnedEventsTimelineController.callbacks - .receive(on: DispatchQueue.main) - .sink { [weak self] callback in + pinnedEventsTimelineProvider.updatePublisher + .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) + .sink { [weak self] updatedItems, _ in guard let self else { return } - - switch callback { - case .updatedTimelineItems(let updatedItems, _): - buildPinnedEventsContent(timelineItems: updatedItems) - default: - break - } + buildPinnedEventsContent(timelineItems: updatedItems) } .store(in: &cancellables) + + self.pinnedEventsTimelineController = pinnedEventsTimelineController } } @@ -660,17 +658,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol // MARK: - Timeline Item Building - private func buildPinnedEventsContent(timelineItems: [RoomTimelineItemProtocol]) { - var timelineItemsDictionary = OrderedDictionary() + private func buildPinnedEventsContent(timelineItems: [TimelineItemProxy]) { + var timelineItemsDictionary = OrderedDictionary() for item in timelineItems { // Only remote events are pinned - if let eventID = item.id.eventID { - timelineItemsDictionary.updateValue(.init(item: item), forKey: eventID) + if case let .event(event) = item, + let eventID = event.id.eventID { + timelineItemsDictionary.updateValue(pinnedEventStringBuilder.buildAttributedString(for: event) ?? AttributedString(L10n.commonUnsupportedEvent), + forKey: eventID) } } - state.pinnedEventsState.pinnedEvents = timelineItemsDictionary + state.pinnedEventsState.pinnedEventsContent = timelineItemsDictionary } private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) { @@ -879,7 +879,6 @@ extension RoomScreenViewModel { static let mock = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "Preview room")), focussedEventID: nil, timelineController: MockRoomTimelineController(), - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift b/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift index 46fa28e3d1..1d46fb84b9 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift @@ -27,7 +27,7 @@ struct PinnedItemsBannerView: View { let index = pinnedEventsState.selectedPinIndex + 1 let boldPlaceholder = "{bold}" var finalString = AttributedString(L10n.screenRoomPinnedBannerIndicatorDescription(boldPlaceholder)) - var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, pinnedEventsState.pinnedEvents.count)) + var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, pinnedEventsState.pinnedEventsContent.count)) boldString.bold() finalString.replace(boldPlaceholder, with: boldString) return finalString @@ -48,7 +48,7 @@ struct PinnedItemsBannerView: View { Button { onMainButtonTap() } label: { HStack(spacing: 0) { HStack(spacing: 10) { - PinnedItemsIndicatorView(pinIndex: pinnedEventsState.selectedPinIndex, pinsCount: pinnedEventsState.pinnedEvents.count) + PinnedItemsIndicatorView(pinIndex: pinnedEventsState.selectedPinIndex, pinsCount: pinnedEventsState.pinnedEventsContent.count) .accessibilityHidden(true) CompoundIcon(\.pinSolid, size: .small, relativeTo: .compound.bodyMD) .foregroundColor(Color.compound.iconSecondaryAlpha) @@ -87,7 +87,10 @@ struct PinnedItemsBannerView: View { struct PinnedItemsBannerView_Previews: PreviewProvider, TestablePreview { static var previews: some View { - PinnedItemsBannerView(pinnedEventsState: .init(pinnedEvents: [:], selectedPinEventID: "Content"), + PinnedItemsBannerView(pinnedEventsState: .init(pinnedEventsContent: ["1": "Content", + "2": "2", + "3": "3"], + selectedPinEventID: "1"), onMainButtonTap: { }, onViewAllButtonTap: { }) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift b/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift index 6efc932363..165c8aae93 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/ReadReceipts/ReadReceiptsSummaryView.swift @@ -54,7 +54,6 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview { let roomProxyMock = RoomProxyMock(.init(name: "Room", members: members)) let mock = RoomScreenViewModel(roomProxy: roomProxyMock, timelineController: MockRoomTimelineController(), - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 66e15cbdfa..8ba8124ef2 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -209,7 +209,6 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview { name: "Preview room", hasOngoingCall: true)), timelineController: MockRoomTimelineController(), - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift index 81d37ab08f..e4933ee342 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineReadReceiptsView.swift @@ -92,7 +92,6 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview { static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "Test", members: members)), timelineController: MockRoomTimelineController(), - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift index 57187e2c5a..3152077a99 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/HighlightedTimelineItemModifier.swift @@ -97,7 +97,6 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider { static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(name: "Preview room")), focussedEventID: focussedEventID, timelineController: MockRoomTimelineController(), - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift index 7e4a3f3dc2..5a0b15423a 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineView.swift @@ -82,7 +82,6 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview { static let viewModel = RoomScreenViewModel(roomProxy: RoomProxyMock(.init(id: "stable_id", name: "Preview room")), timelineController: MockRoomTimelineController(), - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 95f063c6e7..e030903329 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -759,10 +759,11 @@ class ClientProxy: ClientProxyProtocol { let roomListService = syncService.roomListService() let roomMessageEventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(cacheKey: "roomList", - mentionBuilder: PlainMentionBuilder())) + mentionBuilder: PlainMentionBuilder()), prefix: .senderName) let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID, shouldDisambiguateDisplayNames: false), messageEventStringBuilder: roomMessageEventStringBuilder, - shouldDisambiguateDisplayNames: false) + shouldDisambiguateDisplayNames: false, + shouldPrefixSenderName: true) roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService, eventStringBuilder: eventStringBuilder, diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift index 3878f83bc4..f07c1d3197 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift @@ -21,6 +21,7 @@ struct RoomEventStringBuilder { let stateEventStringBuilder: RoomStateEventStringBuilder let messageEventStringBuilder: RoomMessageEventStringBuilder let shouldDisambiguateDisplayNames: Bool + let shouldPrefixSenderName: Bool func buildAttributedString(for eventItemProxy: EventTimelineItemProxy) -> AttributedString? { let sender = eventItemProxy.sender @@ -50,7 +51,7 @@ struct RoomEventStringBuilder { } let messageType = messageContent.msgtype() - return messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName, prefixWithSenderName: true) + return messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName) case .state(_, let state): return stateEventStringBuilder .buildString(for: state, sender: sender, isOutgoing: isOutgoing) @@ -78,6 +79,9 @@ struct RoomEventStringBuilder { } private func prefix(_ eventSummary: String, with senderDisplayName: String) -> AttributedString { + guard shouldPrefixSenderName else { + return AttributedString(eventSummary) + } let attributedEventSummary = AttributedString(eventSummary.trimmingCharacters(in: .whitespacesAndNewlines)) var attributedSenderDisplayName = AttributedString(senderDisplayName) @@ -86,4 +90,13 @@ struct RoomEventStringBuilder { // Don't include the message body in the markdown otherwise it makes tappable links. return attributedSenderDisplayName + ": " + attributedEventSummary } + + static func pinnedEventStringBuilder(userID: String) -> Self { + RoomEventStringBuilder(stateEventStringBuilder: .init(userID: userID, + shouldDisambiguateDisplayNames: false), + messageEventStringBuilder: .init(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()), + prefix: .mediaType), + shouldDisambiguateDisplayNames: false, + shouldPrefixSenderName: false) + } } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift index 86ba1481d7..90ae993043 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift @@ -18,10 +18,17 @@ import Foundation import MatrixRustSDK struct RoomMessageEventStringBuilder { + enum Prefix { + case senderName + case mediaType + case none + } + let attributedStringBuilder: AttributedStringBuilderProtocol + let prefix: Prefix - func buildAttributedString(for messageType: MessageType, senderDisplayName: String, prefixWithSenderName: Bool) -> AttributedString { - let message: String + func buildAttributedString(for messageType: MessageType, senderDisplayName: String) -> AttributedString { + let message: AttributedString switch messageType { // Message types that don't need a prefix. case .emote(content: let content): @@ -33,46 +40,54 @@ struct RoomMessageEventStringBuilder { // Message types that should be prefixed with the sender's name. case .audio(content: let content): let isVoiceMessage = content.voice != nil - message = isVoiceMessage ? L10n.commonVoiceMessage : L10n.commonAudio + var content = AttributedString(isVoiceMessage ? L10n.commonVoiceMessage : L10n.commonAudio) + if prefix == .mediaType { + content.bold() + } + message = content case .image(let content): - message = "\(L10n.commonImage) - \(content.body)" + message = prefix == .mediaType ? prefix(AttributedString(content.body), with: L10n.commonImage) : AttributedString("\(L10n.commonImage) - \(content.body)") case .video(let content): - message = "\(L10n.commonVideo) - \(content.body)" + message = prefix == .mediaType ? prefix(AttributedString(content.body), with: L10n.commonVideo) : AttributedString("\(L10n.commonVideo) - \(content.body)") case .file(let content): - message = "\(L10n.commonFile) - \(content.body)" + message = prefix == .mediaType ? prefix(AttributedString(content.body), with: L10n.commonFile) : AttributedString("\(L10n.commonFile) - \(content.body)") case .location: - message = L10n.commonSharedLocation + var content = AttributedString(L10n.commonSharedLocation) + if prefix == .mediaType { + content.bold() + } + message = content case .notice(content: let content): if let attributedMessage = attributedMessageFrom(formattedBody: content.formatted) { - message = String(attributedMessage.characters) + message = attributedMessage } else { - message = content.body + message = AttributedString(content.body) } case .text(content: let content): if let attributedMessage = attributedMessageFrom(formattedBody: content.formatted) { - message = String(attributedMessage.characters) + message = attributedMessage } else { - message = content.body + message = AttributedString(content.body) } case .other(_, let body): - message = body + message = AttributedString(body) } - if prefixWithSenderName { + if prefix == .senderName { return prefix(message, with: senderDisplayName) } else { - return AttributedString(message) + return message } } - private func prefix(_ eventSummary: String, with senderDisplayName: String) -> AttributedString { - let attributedEventSummary = AttributedString(eventSummary.trimmingCharacters(in: .whitespacesAndNewlines)) + private func prefix(_ eventSummary: AttributedString, with textToBold: String) -> AttributedString { + let attributedEventSummary = AttributedString(eventSummary.string.trimmingCharacters(in: .whitespacesAndNewlines)) - var attributedSenderDisplayName = AttributedString(senderDisplayName) - attributedSenderDisplayName.bold() + var attributedPrefix = AttributedString(textToBold + ":") + attributedPrefix.bold() // Don't include the message body in the markdown otherwise it makes tappable links. - return attributedSenderDisplayName + ": " + attributedEventSummary + return attributedPrefix + " " + attributedEventSummary } private func attributedMessageFrom(formattedBody: FormattedBody?) -> AttributedString? { diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift index 582e43e2ce..182fe417d5 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactory.swift @@ -26,12 +26,4 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol { timelineItemFactory: timelineItemFactory, appSettings: ServiceLocator.shared.settings) } - - func buildPinnedEventsTimelineController(roomProxy: RoomProxyProtocol, - timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? { - guard let pinnedEventsTimeline = await roomProxy.pinnedEventsTimeline else { - return nil - } - return RoomTimelineController(roomProxy: roomProxy, timelineProxy: pinnedEventsTimeline, initialFocussedEventID: nil, timelineItemFactory: timelineItemFactory, appSettings: ServiceLocator.shared.settings) - } } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift index e7e2469761..3a5d9d5512 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerFactoryProtocol.swift @@ -21,9 +21,6 @@ protocol RoomTimelineControllerFactoryProtocol { func buildRoomTimelineController(roomProxy: RoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol - - func buildPinnedEventsTimelineController(roomProxy: RoomProxyProtocol, - timelineItemFactory: RoomTimelineItemFactoryProtocol) async -> RoomTimelineControllerProtocol? } // sourcery: AutoMockable diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 145a58b967..81eb828633 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -250,7 +250,6 @@ class MockScreen: Identifiable { let navigationStackCoordinator = NavigationStackCoordinator() let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Some room name", avatarURL: nil)), timelineController: MockRoomTimelineController(), - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -268,7 +267,6 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -286,7 +284,6 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.default let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -304,7 +301,6 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.smallChunkWithReadReceipts let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "New room", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -325,7 +321,6 @@ class MockScreen: Identifiable { timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Small timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -346,7 +341,6 @@ class MockScreen: Identifiable { timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Small timeline, paginating", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -367,7 +361,6 @@ class MockScreen: Identifiable { timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -389,7 +382,6 @@ class MockScreen: Identifiable { timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -410,7 +402,6 @@ class MockScreen: Identifiable { timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Large timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -430,7 +421,6 @@ class MockScreen: Identifiable { timelineController.timelineItems = RoomTimelineItemFixtures.permalinkChunk let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Timeline highlight", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -464,7 +454,6 @@ class MockScreen: Identifiable { timelineController.incomingItems = [] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -485,7 +474,6 @@ class MockScreen: Identifiable { timelineController.incomingItems = [] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), @@ -506,7 +494,6 @@ class MockScreen: Identifiable { timelineController.incomingItems = [] let parameters = RoomScreenCoordinatorParameters(roomProxy: RoomProxyMock(.init(name: "Polls timeline", avatarURL: URL.picturesDirectory)), timelineController: timelineController, - pinnedTimelineBuilder: .mock(), mediaProvider: MockMediaProvider(), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), diff --git a/NSE/Sources/NotificationContentBuilder.swift b/NSE/Sources/NotificationContentBuilder.swift index 90d7083e74..569d90852f 100644 --- a/NSE/Sources/NotificationContentBuilder.swift +++ b/NSE/Sources/NotificationContentBuilder.swift @@ -103,7 +103,7 @@ struct NotificationContentBuilder { var notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider) let displayName = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName - let message = String(messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName, prefixWithSenderName: false).characters) + let message = String(messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName).characters) notification.body = notificationItem.hasMention ? L10n.notificationMentionedYouBody(message) : message diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift index 172c464e77..2164ff6ee0 100644 --- a/NSE/Sources/NotificationServiceExtension.swift +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -42,7 +42,7 @@ import UserNotifications // database, logging, etc. are only ever setup once per *process* private let settings: NSESettingsProtocol = AppSettings() -private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()))) +private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()), prefix: .none)) private let keychainController = KeychainController(service: .sessions, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) From 2cf9d4db3f385a8f38b2527b09580e6fb04d2823 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 31 Jul 2024 12:11:25 +0200 Subject: [PATCH 4/8] using a cache key --- .../Services/Room/RoomSummary/RoomEventStringBuilder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift index f07c1d3197..4b6e3878ef 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomEventStringBuilder.swift @@ -94,7 +94,7 @@ struct RoomEventStringBuilder { static func pinnedEventStringBuilder(userID: String) -> Self { RoomEventStringBuilder(stateEventStringBuilder: .init(userID: userID, shouldDisambiguateDisplayNames: false), - messageEventStringBuilder: .init(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()), + messageEventStringBuilder: .init(attributedStringBuilder: AttributedStringBuilder(cacheKey: "pinnedEvents", mentionBuilder: PlainMentionBuilder()), prefix: .mediaType), shouldDisambiguateDisplayNames: false, shouldPrefixSenderName: false) From 5fab6abdbccf2d2385497859e0000e20ed430313 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 31 Jul 2024 15:30:40 +0200 Subject: [PATCH 5/8] addressing some PR comments --- .../Screens/RoomScreen/RoomScreenModels.swift | 31 +++++++++-------- .../RoomScreen/RoomScreenViewModel.swift | 22 +++++-------- .../PinnedItemsBannerView.swift | 33 ++++++++++++++----- .../Screens/RoomScreen/View/RoomScreen.swift | 6 ++-- 4 files changed, 52 insertions(+), 40 deletions(-) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 007083bc5e..ce7d21c271 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -139,7 +139,7 @@ enum RoomScreenViewAction { case hasSwitchedTimeline case hasScrolled(direction: ScrollDirection) - case tappedPinBanner + case tappedPinnedEventsBanner case viewAllPins } @@ -177,8 +177,8 @@ struct RoomScreenViewState: BindableState { // This is used to controler the banner var pinnedEventsState = PinnedEventsState() - var shouldShowPinBanner: Bool { - isPinningEnabled && !pinnedEventsState.pinnedEventsContent.isEmpty && lastScrollDirection != .top + var shouldShowPinnedEventsBanner: Bool { + isPinningEnabled && !pinnedEventsState.pinnedEventContents.isEmpty && lastScrollDirection != .top } var canJoinCall = false @@ -299,15 +299,14 @@ enum ScrollDirection: Equatable { } struct PinnedEventsState: Equatable { - // For now these will only contain and show the event IDs, but in the future they will also contain the content - var pinnedEventsContent: OrderedDictionary = [:] { + var pinnedEventContents: OrderedDictionary = [:] { didSet { - if selectedPinEventID == nil, !pinnedEventsContent.keys.isEmpty { - selectedPinEventID = pinnedEventsContent.keys.last - } else if pinnedEventsContent.isEmpty { + if selectedPinEventID == nil, !pinnedEventContents.keys.isEmpty { + selectedPinEventID = pinnedEventContents.keys.last + } else if pinnedEventContents.isEmpty { selectedPinEventID = nil - } else if let selectedPinEventID, !pinnedEventsContent.keys.set.contains(selectedPinEventID) { - self.selectedPinEventID = pinnedEventsContent.firstNonNil { $0.key } + } else if let selectedPinEventID, !pinnedEventContents.keys.set.contains(selectedPinEventID) { + self.selectedPinEventID = pinnedEventContents.firstNonNil { $0.key } } } } @@ -315,11 +314,11 @@ struct PinnedEventsState: Equatable { private(set) var selectedPinEventID: String? var selectedPinIndex: Int { - let defaultValue = pinnedEventsContent.isEmpty ? 0 : pinnedEventsContent.count - 1 + let defaultValue = pinnedEventContents.isEmpty ? 0 : pinnedEventContents.count - 1 guard let selectedPinEventID else { return defaultValue } - return pinnedEventsContent.keys.firstIndex(of: selectedPinEventID) ?? defaultValue + return pinnedEventContents.keys.firstIndex(of: selectedPinEventID) ?? defaultValue } // For now we show the event ID as the content, but is just until we have a way to get the real content @@ -327,15 +326,15 @@ struct PinnedEventsState: Equatable { guard let selectedPinEventID else { return AttributedString() } - return pinnedEventsContent[selectedPinEventID] ?? AttributedString() + return pinnedEventContents[selectedPinEventID] ?? AttributedString() } mutating func nextPin() { - guard !pinnedEventsContent.isEmpty else { + guard !pinnedEventContents.isEmpty else { return } let currentIndex = selectedPinIndex - let nextIndex = (currentIndex + 1) % pinnedEventsContent.count - selectedPinEventID = pinnedEventsContent.keys[nextIndex] + let nextIndex = (currentIndex + 1) % pinnedEventContents.count + selectedPinEventID = pinnedEventContents.keys[nextIndex] } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 88f97d3ec9..2698452c2f 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -49,9 +49,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol private var paginateBackwardsTask: Task? private var paginateForwardsTask: Task? - - // Needs to be stored to allow the provider to keep sending updates. - private var pinnedEventsTimelineController: RoomTimelineControllerProtocol? init(roomProxy: RoomProxyProtocol, focussedEventID: String? = nil, @@ -135,17 +132,16 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol return } - buildPinnedEventsContent(timelineItems: pinnedEventsTimelineProvider.itemProxies) + buildPinnedEventContent(timelineItems: pinnedEventsTimelineProvider.itemProxies) pinnedEventsTimelineProvider.updatePublisher + // When pinning or unpinning an item, the timeline might return empty for a short while, so we need to debounce it to prevent weird UI behaviours like the banner disappearing .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) .sink { [weak self] updatedItems, _ in guard let self else { return } - buildPinnedEventsContent(timelineItems: updatedItems) + buildPinnedEventContent(timelineItems: updatedItems) } .store(in: &cancellables) - - self.pinnedEventsTimelineController = pinnedEventsTimelineController } } @@ -219,7 +215,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol Task { state.timelineViewState.isSwitchingTimelines = false } case let .hasScrolled(direction): state.lastScrollDirection = direction - case .tappedPinBanner: + case .tappedPinnedEventsBanner: if let eventID = state.pinnedEventsState.selectedPinEventID { Task { await focusOnEvent(eventID: eventID) } } @@ -658,19 +654,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol // MARK: - Timeline Item Building - private func buildPinnedEventsContent(timelineItems: [TimelineItemProxy]) { - var timelineItemsDictionary = OrderedDictionary() + private func buildPinnedEventContent(timelineItems: [TimelineItemProxy]) { + var pinnedEventContents = OrderedDictionary() for item in timelineItems { // Only remote events are pinned if case let .event(event) = item, let eventID = event.id.eventID { - timelineItemsDictionary.updateValue(pinnedEventStringBuilder.buildAttributedString(for: event) ?? AttributedString(L10n.commonUnsupportedEvent), - forKey: eventID) + pinnedEventContents.updateValue(pinnedEventStringBuilder.buildAttributedString(for: event) ?? AttributedString(L10n.commonUnsupportedEvent), + forKey: eventID) } } - state.pinnedEventsState.pinnedEventsContent = timelineItemsDictionary + state.pinnedEventsState.pinnedEventContents = pinnedEventContents } private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) { diff --git a/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift b/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift index 1d46fb84b9..e817940d88 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift @@ -27,7 +27,7 @@ struct PinnedItemsBannerView: View { let index = pinnedEventsState.selectedPinIndex + 1 let boldPlaceholder = "{bold}" var finalString = AttributedString(L10n.screenRoomPinnedBannerIndicatorDescription(boldPlaceholder)) - var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, pinnedEventsState.pinnedEventsContent.count)) + var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, pinnedEventsState.pinnedEventContents.count)) boldString.bold() finalString.replace(boldPlaceholder, with: boldString) return finalString @@ -48,7 +48,7 @@ struct PinnedItemsBannerView: View { Button { onMainButtonTap() } label: { HStack(spacing: 0) { HStack(spacing: 10) { - PinnedItemsIndicatorView(pinIndex: pinnedEventsState.selectedPinIndex, pinsCount: pinnedEventsState.pinnedEventsContent.count) + PinnedItemsIndicatorView(pinIndex: pinnedEventsState.selectedPinIndex, pinsCount: pinnedEventsState.pinnedEventContents.count) .accessibilityHidden(true) CompoundIcon(\.pinSolid, size: .small, relativeTo: .compound.bodyMD) .foregroundColor(Color.compound.iconSecondaryAlpha) @@ -86,12 +86,29 @@ struct PinnedItemsBannerView: View { } struct PinnedItemsBannerView_Previews: PreviewProvider, TestablePreview { + static var attributedContent: AttributedString { + var boldPart = AttributedString("Image:") + boldPart.bold() + return boldPart + " content.png" + } + static var previews: some View { - PinnedItemsBannerView(pinnedEventsState: .init(pinnedEventsContent: ["1": "Content", - "2": "2", - "3": "3"], - selectedPinEventID: "1"), - onMainButtonTap: { }, - onViewAllButtonTap: { }) + VStack(spacing: 20) { + PinnedItemsBannerView(pinnedEventsState: .init(pinnedEventContents: ["1": "Content", + "2": "2", + "3": "3"], + selectedPinEventID: "1"), + onMainButtonTap: { }, + onViewAllButtonTap: { }) + PinnedItemsBannerView(pinnedEventsState: .init(pinnedEventContents: ["1": "Very very very very long content here", + "2": "2"], + selectedPinEventID: "1"), + onMainButtonTap: { }, + onViewAllButtonTap: { }) + PinnedItemsBannerView(pinnedEventsState: .init(pinnedEventContents: ["1": attributedContent], + selectedPinEventID: "1"), + onMainButtonTap: { }, + onViewAllButtonTap: { }) + } } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 8ba8124ef2..6d0227d260 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -50,11 +50,11 @@ struct RoomScreen: View { } .overlay(alignment: .top) { Group { - if context.viewState.shouldShowPinBanner { + if context.viewState.shouldShowPinnedEventsBanner { pinnedItemsBanner } } - .animation(.elementDefault, value: context.viewState.shouldShowPinBanner) + .animation(.elementDefault, value: context.viewState.shouldShowPinnedEventsBanner) } .navigationTitle(L10n.screenRoomTitle) // Hidden but used for back button text. .navigationBarTitleDisplayMode(.inline) @@ -111,7 +111,7 @@ struct RoomScreen: View { private var pinnedItemsBanner: some View { PinnedItemsBannerView(pinnedEventsState: context.viewState.pinnedEventsState, - onMainButtonTap: { context.send(viewAction: .tappedPinBanner) }, + onMainButtonTap: { context.send(viewAction: .tappedPinnedEventsBanner) }, onViewAllButtonTap: { context.send(viewAction: .viewAllPins) }) .transition(.move(edge: .top)) } From 9b44a63d04cb7966cd1db8f0959528cd0d9c52f2 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 31 Jul 2024 16:12:29 +0200 Subject: [PATCH 6/8] updated tests --- .../Screens/RoomScreen/RoomScreenModels.swift | 17 ++++++++++++++--- .../PinnedItemsBannerView.swift | 17 +++++------------ .../test_pinnedItemsBannerView-iPad-en-GB.1.png | 4 ++-- ...test_pinnedItemsBannerView-iPad-pseudo.1.png | 4 ++-- ..._pinnedItemsBannerView-iPhone-15-en-GB.1.png | 4 ++-- ...pinnedItemsBannerView-iPhone-15-pseudo.1.png | 4 ++-- 6 files changed, 27 insertions(+), 23 deletions(-) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index ce7d21c271..c1d12bcbcd 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -321,12 +321,23 @@ struct PinnedEventsState: Equatable { return pinnedEventContents.keys.firstIndex(of: selectedPinEventID) ?? defaultValue } - // For now we show the event ID as the content, but is just until we have a way to get the real content var selectedPinContent: AttributedString { - guard let selectedPinEventID else { + guard let selectedPinEventID, + var content = pinnedEventContents[selectedPinEventID] else { return AttributedString() } - return pinnedEventContents[selectedPinEventID] ?? AttributedString() + content.font = .compound.bodyMD + return content + } + + var bannerIndicatorDescription: AttributedString { + let index = selectedPinIndex + 1 + let boldPlaceholder = "{bold}" + var finalString = AttributedString(L10n.screenRoomPinnedBannerIndicatorDescription(boldPlaceholder)) + var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, pinnedEventContents.count)) + boldString.bold() + finalString.replace(boldPlaceholder, with: boldString) + return finalString } mutating func nextPin() { diff --git a/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift b/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift index e817940d88..f022b46f0e 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/PinnedItemsBanner/PinnedItemsBannerView.swift @@ -23,16 +23,6 @@ struct PinnedItemsBannerView: View { let onMainButtonTap: () -> Void let onViewAllButtonTap: () -> Void - private var bannerIndicatorDescription: AttributedString { - let index = pinnedEventsState.selectedPinIndex + 1 - let boldPlaceholder = "{bold}" - var finalString = AttributedString(L10n.screenRoomPinnedBannerIndicatorDescription(boldPlaceholder)) - var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, pinnedEventsState.pinnedEventContents.count)) - boldString.bold() - finalString.replace(boldPlaceholder, with: boldString) - return finalString - } - var body: some View { HStack(spacing: 0) { mainButton @@ -73,7 +63,7 @@ struct PinnedItemsBannerView: View { private var content: some View { VStack(alignment: .leading, spacing: 0) { - Text(bannerIndicatorDescription) + Text(pinnedEventsState.bannerIndicatorDescription) .font(.compound.bodySM) .foregroundColor(.compound.textActionAccent) .lineLimit(1) @@ -89,7 +79,10 @@ struct PinnedItemsBannerView_Previews: PreviewProvider, TestablePreview { static var attributedContent: AttributedString { var boldPart = AttributedString("Image:") boldPart.bold() - return boldPart + " content.png" + var final = boldPart + " content.png" + // This should be ignored when presented + final.font = .headline + return final } static var previews: some View { diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPad-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPad-en-GB.1.png index 98e5568741..92c1d53b36 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPad-en-GB.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPad-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08962f7405eacac00d0fbc410e806820202105b03558fbc962ab4d5ba53f0293 -size 85241 +oid sha256:5eb75e371a78b17fad61215006b871310665bf8061772985d047facbccc5bae7 +size 129176 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPad-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPad-pseudo.1.png index 4990d784f5..7019c67c6f 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPad-pseudo.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPad-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9cb3d02aa9af3d6d2fcd8d5d1195ab8c7cb9dcb1a7544fe5eceb468638770009 -size 94063 +oid sha256:745d8fdea278dc6ab55f39501384ecc5efc3899db233c52c797a8542f4885adc +size 154132 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-en-GB.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-en-GB.1.png index 7c0822bd44..0aeb4f05d1 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-en-GB.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22228167b8b988f91baac100e980d41039a3b942be219cdfa7d8a9bfd8947610 -size 42906 +oid sha256:82dc33b9de9b497388b91304afe2623e4fa5c9d5aff246a87b68c4d948a63a37 +size 76715 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-pseudo.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-pseudo.1.png index cf9afef3be..51383825dc 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-pseudo.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:116c3b0f009cbcb102fa574605577da8d2d68f95d33746f778b99afb19cec872 -size 46568 +oid sha256:48df9215b99d69572bf3c4395e4b13d0b34012691b98bba75c73fc35c3b0b6fb +size 85989 From 4ba3ceda8e589e8f77a35de419c1acb05b3696e4 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Mon, 5 Aug 2024 11:25:33 +0200 Subject: [PATCH 7/8] updated the SDK --- .../Mocks/Generated/SDKGeneratedMocks.swift | 88 +++++++++++++------ .../Sources/Services/Room/RoomProxy.swift | 2 +- 2 files changed, 63 insertions(+), 27 deletions(-) diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index 7db135cb5a..056491d7f7 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -10989,6 +10989,42 @@ open class RoomSDKMock: MatrixRustSDK.Room { try await clearComposerDraftClosure?() } + //MARK: - clearPinnedEventsCache + + var clearPinnedEventsCacheUnderlyingCallsCount = 0 + open var clearPinnedEventsCacheCallsCount: Int { + get { + if Thread.isMainThread { + return clearPinnedEventsCacheUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = clearPinnedEventsCacheUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + clearPinnedEventsCacheUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + clearPinnedEventsCacheUnderlyingCallsCount = newValue + } + } + } + } + open var clearPinnedEventsCacheCalled: Bool { + return clearPinnedEventsCacheCallsCount > 0 + } + open var clearPinnedEventsCacheClosure: (() async -> Void)? + + open override func clearPinnedEventsCache() async { + clearPinnedEventsCacheCallsCount += 1 + await clearPinnedEventsCacheClosure?() + } + //MARK: - discardRoomKey open var discardRoomKeyThrowableError: Error? @@ -13007,16 +13043,16 @@ open class RoomSDKMock: MatrixRustSDK.Room { //MARK: - pinnedEventsTimeline - open var pinnedEventsTimelineInternalIdPrefixThrowableError: Error? - var pinnedEventsTimelineInternalIdPrefixUnderlyingCallsCount = 0 - open var pinnedEventsTimelineInternalIdPrefixCallsCount: Int { + open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadThrowableError: Error? + var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount = 0 + open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCallsCount: Int { get { if Thread.isMainThread { - return pinnedEventsTimelineInternalIdPrefixUnderlyingCallsCount + return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = pinnedEventsTimelineInternalIdPrefixUnderlyingCallsCount + returnValue = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount } return returnValue! @@ -13024,29 +13060,29 @@ open class RoomSDKMock: MatrixRustSDK.Room { } set { if Thread.isMainThread { - pinnedEventsTimelineInternalIdPrefixUnderlyingCallsCount = newValue + pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - pinnedEventsTimelineInternalIdPrefixUnderlyingCallsCount = newValue + pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount = newValue } } } } - open var pinnedEventsTimelineInternalIdPrefixCalled: Bool { - return pinnedEventsTimelineInternalIdPrefixCallsCount > 0 + open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCalled: Bool { + return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCallsCount > 0 } - open var pinnedEventsTimelineInternalIdPrefixReceivedInternalIdPrefix: String? - open var pinnedEventsTimelineInternalIdPrefixReceivedInvocations: [String?] = [] + open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedArguments: (internalIdPrefix: String?, maxEventsToLoad: UInt16)? + open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedInvocations: [(internalIdPrefix: String?, maxEventsToLoad: UInt16)] = [] - var pinnedEventsTimelineInternalIdPrefixUnderlyingReturnValue: Timeline! - open var pinnedEventsTimelineInternalIdPrefixReturnValue: Timeline! { + var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue: Timeline! + open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReturnValue: Timeline! { get { if Thread.isMainThread { - return pinnedEventsTimelineInternalIdPrefixUnderlyingReturnValue + return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue } else { var returnValue: Timeline? = nil DispatchQueue.main.sync { - returnValue = pinnedEventsTimelineInternalIdPrefixUnderlyingReturnValue + returnValue = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue } return returnValue! @@ -13054,29 +13090,29 @@ open class RoomSDKMock: MatrixRustSDK.Room { } set { if Thread.isMainThread { - pinnedEventsTimelineInternalIdPrefixUnderlyingReturnValue = newValue + pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - pinnedEventsTimelineInternalIdPrefixUnderlyingReturnValue = newValue + pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue = newValue } } } } - open var pinnedEventsTimelineInternalIdPrefixClosure: ((String?) async throws -> Timeline)? + open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure: ((String?, UInt16) async throws -> Timeline)? - open override func pinnedEventsTimeline(internalIdPrefix: String?) async throws -> Timeline { - if let error = pinnedEventsTimelineInternalIdPrefixThrowableError { + open override func pinnedEventsTimeline(internalIdPrefix: String?, maxEventsToLoad: UInt16) async throws -> Timeline { + if let error = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadThrowableError { throw error } - pinnedEventsTimelineInternalIdPrefixCallsCount += 1 - pinnedEventsTimelineInternalIdPrefixReceivedInternalIdPrefix = internalIdPrefix + pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCallsCount += 1 + pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedArguments = (internalIdPrefix: internalIdPrefix, maxEventsToLoad: maxEventsToLoad) DispatchQueue.main.async { - self.pinnedEventsTimelineInternalIdPrefixReceivedInvocations.append(internalIdPrefix) + self.pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedInvocations.append((internalIdPrefix: internalIdPrefix, maxEventsToLoad: maxEventsToLoad)) } - if let pinnedEventsTimelineInternalIdPrefixClosure = pinnedEventsTimelineInternalIdPrefixClosure { - return try await pinnedEventsTimelineInternalIdPrefixClosure(internalIdPrefix) + if let pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure { + return try await pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure(internalIdPrefix, maxEventsToLoad) } else { - return pinnedEventsTimelineInternalIdPrefixReturnValue + return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReturnValue } } diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index dea915945e..7067d175f0 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -30,7 +30,7 @@ class RoomProxy: RoomProxyProtocol { return innerPinnedEventsTimeline } else { do { - let timeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: nil), isLive: false) + let timeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: nil, maxEventsToLoad: 100), isLive: false) await timeline.subscribeForUpdates() innerPinnedEventsTimeline = timeline return timeline From 431a90cb48f5b89b3a29e089f609f2434a22a1bb Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Mon, 5 Aug 2024 14:08:30 +0200 Subject: [PATCH 8/8] improved comments --- ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index c1d12bcbcd..77d9bcc479 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -172,9 +172,10 @@ struct RoomScreenViewState: BindableState { var isPinningEnabled = false var lastScrollDirection: ScrollDirection? - // The `pinnedEventIDs` is used only to determine if an item can be pinned or not since it does not depend on the pinned events timeline, but on the room info update which is faster + // The `pinnedEventIDs` are used only to determine if an item is already pinned or not. + // It's updated from the room info, so it's faster than using the timeline var pinnedEventIDs: Set = [] - // This is used to controler the banner + // This is used to control the banner var pinnedEventsState = PinnedEventsState() var shouldShowPinnedEventsBanner: Bool {