mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +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 */; };
|
6A54F52443EC52AC5CD772C0 /* JoinRoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 869A8A4632E511351BFE2EC4 /* JoinRoomScreen.swift */; };
|
||||||
6A5FDF9306CBD62C7EDDB552 /* CLLocationManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C723327DC4A3093CD9675B27 /* CLLocationManagerMock.swift */; };
|
6A5FDF9306CBD62C7EDDB552 /* CLLocationManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C723327DC4A3093CD9675B27 /* CLLocationManagerMock.swift */; };
|
||||||
6A64546ABE648ED9E6DBB459 /* RemoteSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D186A6DB8FAC5C9D0E4D61 /* RemoteSettingsHook.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 */; };
|
6AB306367E56A6F6DFA0E2FF /* RoomSummaryProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46E441BA50705E6CEC89FE0 /* RoomSummaryProviderTests.swift */; };
|
||||||
6AC798F52571BE495E6AA1CE /* OAuthAccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21819C86A022D6ADBA7A7A15 /* OAuthAccountSettingsPresenter.swift */; };
|
6AC798F52571BE495E6AA1CE /* OAuthAccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21819C86A022D6ADBA7A7A15 /* OAuthAccountSettingsPresenter.swift */; };
|
||||||
6AD722DD92E465E56D2885AB /* BugReportScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA919F521E9F0EE3638AFC85 /* BugReportScreen.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
FE5CD2993048222B64C45006 /* SDKListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKListener.swift; sourceTree = "<group>"; };
|
||||||
@@ -5343,6 +5345,7 @@
|
|||||||
18B223FA339BF53085328DEE /* SpaceScreenViewModelTests.swift */,
|
18B223FA339BF53085328DEE /* SpaceScreenViewModelTests.swift */,
|
||||||
6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */,
|
6DF438EAFC732D2D95D34BF6 /* StartChatViewModelTests.swift */,
|
||||||
2CEBCB9676FCD1D0F13188DD /* StringTests.swift */,
|
2CEBCB9676FCD1D0F13188DD /* StringTests.swift */,
|
||||||
|
FE07EC9D34EF91795EDB3CC4 /* TimelineControllerTests.swift */,
|
||||||
53C65A839BC897F6C236E8A5 /* TimelineInteractionHandlerTests.swift */,
|
53C65A839BC897F6C236E8A5 /* TimelineInteractionHandlerTests.swift */,
|
||||||
9AA3AF94A06D319BB37E52DA /* TimelineItemFactoryTests.swift */,
|
9AA3AF94A06D319BB37E52DA /* TimelineItemFactoryTests.swift */,
|
||||||
C98C243A90369929D669873D /* TimelineItemMenuActionProviderTests.swift */,
|
C98C243A90369929D669873D /* TimelineItemMenuActionProviderTests.swift */,
|
||||||
@@ -8464,6 +8467,7 @@
|
|||||||
6189B4ABD535CE526FA1107B /* StartChatViewModelTests.swift in Sources */,
|
6189B4ABD535CE526FA1107B /* StartChatViewModelTests.swift in Sources */,
|
||||||
1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */,
|
1FEC0A4EC6E6DF693C16B32A /* StringTests.swift in Sources */,
|
||||||
E75CE800B3E64D0F7F8E228D /* TemplateScreenViewModelTests.swift in Sources */,
|
E75CE800B3E64D0F7F8E228D /* TemplateScreenViewModelTests.swift in Sources */,
|
||||||
|
6AA27078438185AEA8CDE2A1 /* TimelineControllerTests.swift in Sources */,
|
||||||
ECE4E03D17E309724013F825 /* TimelineInteractionHandlerTests.swift in Sources */,
|
ECE4E03D17E309724013F825 /* TimelineInteractionHandlerTests.swift in Sources */,
|
||||||
0D4EB2ABAA5FE8CB10FDBCB8 /* TimelineItemFactoryTests.swift in Sources */,
|
0D4EB2ABAA5FE8CB10FDBCB8 /* TimelineItemFactoryTests.swift in Sources */,
|
||||||
0668BECCF11E8EE6936CDF16 /* TimelineItemMenuActionProviderTests.swift in Sources */,
|
0668BECCF11E8EE6936CDF16 /* TimelineItemMenuActionProviderTests.swift in Sources */,
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ class TimelineController: TimelineControllerProtocol {
|
|||||||
activeTimeline = timelineProxy
|
activeTimeline = timelineProxy
|
||||||
activeTimelineItemProvider = liveTimelineItemProvider
|
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()
|
configureActiveTimelineItemProvider()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -765,3 +766,12 @@ private nonisolated extension TimelineItemProxy {
|
|||||||
return false
|
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