mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
Focus events on the live timeline when it has already loaded them
Opening a room on an event always built a detached timeline, even when the live timeline already contained that event, e.g. when tapping the notification for the newest message in a room. The timeline controller now stays on the live timeline in that case, scrolling to and highlighting the event, and only builds a detached timeline when the event isn't loaded.
This commit is contained in:
@@ -675,6 +675,7 @@
|
||||
6A54F52443EC52AC5CD772C0 /* JoinRoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 869A8A4632E511351BFE2EC4 /* JoinRoomScreen.swift */; };
|
||||
6A5FDF9306CBD62C7EDDB552 /* CLLocationManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C723327DC4A3093CD9675B27 /* CLLocationManagerMock.swift */; };
|
||||
6A64546ABE648ED9E6DBB459 /* RemoteSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D186A6DB8FAC5C9D0E4D61 /* RemoteSettingsHook.swift */; };
|
||||
6AA27078438185AEA8CDE2A1 /* TimelineControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE07EC9D34EF91795EDB3CC4 /* TimelineControllerTests.swift */; };
|
||||
6AB306367E56A6F6DFA0E2FF /* RoomSummaryProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46E441BA50705E6CEC89FE0 /* RoomSummaryProviderTests.swift */; };
|
||||
6AC798F52571BE495E6AA1CE /* OAuthAccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21819C86A022D6ADBA7A7A15 /* OAuthAccountSettingsPresenter.swift */; };
|
||||
6AD722DD92E465E56D2885AB /* BugReportScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */; };
|
||||
@@ -3275,6 +3276,7 @@
|
||||
FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerTests.swift; sourceTree = "<group>"; };
|
||||
FDEDD4D2DE0646DA724985D5 /* QRCodeLoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenModels.swift; sourceTree = "<group>"; };
|
||||
FDF73F49E6B6683F7E2D26F0 /* SecureBackupScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
FE07EC9D34EF91795EDB3CC4 /* TimelineControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineControllerTests.swift; sourceTree = "<group>"; };
|
||||
FE1E6FAA3719E9B7A2D5510B /* FormattingToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattingToolbar.swift; sourceTree = "<group>"; };
|
||||
FE3AB4D399D39FBD78119011 /* AuthenticationStartScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenModels.swift; sourceTree = "<group>"; };
|
||||
FE5CD2993048222B64C45006 /* SDKListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKListener.swift; sourceTree = "<group>"; };
|
||||
@@ -5343,6 +5345,7 @@
|
||||
18B223FA339BF53085328DEE /* SpaceScreenViewModelTests.swift */,
|
||||
6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */,
|
||||
2CEBCB9676FCD1D0F13188DD /* StringTests.swift */,
|
||||
FE07EC9D34EF91795EDB3CC4 /* TimelineControllerTests.swift */,
|
||||
53C65A839BC897F6C236E8A5 /* TimelineInteractionHandlerTests.swift */,
|
||||
9AA3AF94A06D319BB37E52DA /* TimelineItemFactoryTests.swift */,
|
||||
C98C243A90369929D669873D /* TimelineItemMenuActionProviderTests.swift */,
|
||||
@@ -8464,6 +8467,7 @@
|
||||
6189B4ABD535CE526FA1107B /* StartChatViewModelTests.swift in Sources */,
|
||||
1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */,
|
||||
E75CE800B3E64D0F7F8E228D /* TemplateScreenViewModelTests.swift in Sources */,
|
||||
6AA27078438185AEA8CDE2A1 /* TimelineControllerTests.swift in Sources */,
|
||||
ECE4E03D17E309724013F825 /* TimelineInteractionHandlerTests.swift in Sources */,
|
||||
0D4EB2ABAA5FE8CB10FDBCB8 /* TimelineItemFactoryTests.swift in Sources */,
|
||||
0668BECCF11E8EE6936CDF16 /* TimelineItemMenuActionProviderTests.swift in Sources */,
|
||||
|
||||
@@ -63,7 +63,8 @@ class TimelineController: TimelineControllerProtocol {
|
||||
activeTimeline = timelineProxy
|
||||
activeTimelineItemProvider = liveTimelineItemProvider
|
||||
|
||||
guard let initialFocussedEventID else {
|
||||
// No need for a detached timeline when the live one has already loaded the event.
|
||||
guard let initialFocussedEventID, !liveTimelineItemProvider.containsEvent(initialFocussedEventID) else {
|
||||
configureActiveTimelineItemProvider()
|
||||
return
|
||||
}
|
||||
@@ -765,3 +766,12 @@ private nonisolated extension TimelineItemProxy {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private extension TimelineItemProviderProtocol {
|
||||
func containsEvent(_ eventID: String) -> Bool {
|
||||
itemProxies.contains { item in
|
||||
guard case let .event(eventItem) = item else { return false }
|
||||
return eventItem.id.eventID == eventID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
@testable import ElementX
|
||||
import MatrixRustSDK
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
struct TimelineControllerTests {
|
||||
private let roomProxy = JoinedRoomProxyMock(.init())
|
||||
|
||||
@Test
|
||||
func initialFocusOnLoadedEventUsesTheLiveTimeline() async throws {
|
||||
let controller = makeController(liveEventIDs: ["$event"], initialFocussedEventID: "$event")
|
||||
try await waitForTimelineItems(controller)
|
||||
|
||||
#expect(!roomProxy.timelineFocusedOnEventEventIDNumberOfEventsCalled)
|
||||
#expect(controller.timelineKind == .live)
|
||||
}
|
||||
|
||||
@Test
|
||||
func initialFocusOnUnloadedEventLoadsADetachedTimeline() async throws {
|
||||
let controller = makeController(liveEventIDs: ["$other"], initialFocussedEventID: "$event")
|
||||
try await waitForTimelineItems(controller)
|
||||
|
||||
#expect(roomProxy.timelineFocusedOnEventEventIDNumberOfEventsReceivedArguments?.eventID == "$event")
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func makeController(liveEventIDs: [String], initialFocussedEventID: String) -> TimelineController {
|
||||
let itemProxies = liveEventIDs.enumerated().map { index, eventID in
|
||||
TimelineItemProxy.event(EventTimelineItemProxy(item: .init(configuration: .init(eventID: eventID)), uniqueID: .init("\(index)")))
|
||||
}
|
||||
let paginationState = TimelinePaginationState(backward: .idle, forward: .endReached)
|
||||
|
||||
let provider = TimelineItemProviderMock()
|
||||
provider.kind = .live
|
||||
provider.itemProxies = itemProxies
|
||||
provider.paginationState = paginationState
|
||||
provider.membershipChangePublisher = PassthroughSubject().eraseToAnyPublisher()
|
||||
provider.updatePublisher = CurrentValueSubject((itemProxies, paginationState)).eraseToAnyPublisher()
|
||||
|
||||
roomProxy.timelineFocusedOnEventEventIDNumberOfEventsReturnValue = .failure(.eventNotFound)
|
||||
|
||||
return TimelineController(roomProxy: roomProxy,
|
||||
timelineProxy: TimelineProxyMock(.init(timelineItemProvider: provider)),
|
||||
initialFocussedEventID: initialFocussedEventID,
|
||||
timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org",
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: "@alice:matrix.org")),
|
||||
mediaProvider: MediaProviderMock(.init()),
|
||||
userSettings: .volatile())
|
||||
}
|
||||
|
||||
/// The live timeline's items are only published once any initial focus attempt has finished.
|
||||
private func waitForTimelineItems(_ controller: TimelineController) async throws {
|
||||
let deferred = deferFulfillment(controller.callbacks) { callback in
|
||||
if case .updatedTimelineItems = callback {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user