Permalink Tweaks (#2768)

* Revert back to creating a new flow when re-selecting the same room.

Fix the composer bug by adding IDs to the all of the module AnyViews.

* Don't show the live timeline when starting with a focussed event.
This commit is contained in:
Doug
2024-04-30 17:25:32 +01:00
committed by GitHub
parent 7c94ed98a5
commit f688d2eda4
15 changed files with 203 additions and 35 deletions
+4 -4
View File
@@ -102,7 +102,6 @@
1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */; }; 1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */; };
157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */; }; 157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */; };
1583E2D766E4485FF91662FC /* PermalinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3EB5B1848CF4F64E63C6B7 /* PermalinkTests.swift */; }; 1583E2D766E4485FF91662FC /* PermalinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3EB5B1848CF4F64E63C6B7 /* PermalinkTests.swift */; };
158A2D528CC78C4E7A8ED608 /* MockRoomTimelineControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71556206CD5E8B1F53F07178 /* MockRoomTimelineControllerFactory.swift */; };
167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */; }; 167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */; };
16CBD087038DE3815CDA512C /* PollMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D38391154120264910D19528 /* PollMock.swift */; }; 16CBD087038DE3815CDA512C /* PollMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D38391154120264910D19528 /* PollMock.swift */; };
1702981A8085BE4FB0EC001B /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33116993D54FADC0C721C1F /* Application.swift */; }; 1702981A8085BE4FB0EC001B /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33116993D54FADC0C721C1F /* Application.swift */; };
@@ -749,6 +748,7 @@
B22D857D1E8FCA6DD74A58E3 /* UserSessionScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F899D02CF26EA7675EEBE74C /* UserSessionScreenTests.swift */; }; B22D857D1E8FCA6DD74A58E3 /* UserSessionScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F899D02CF26EA7675EEBE74C /* UserSessionScreenTests.swift */; };
B245583C63F8F90357B87FAE /* KZFileWatchers in Frameworks */ = {isa = PBXBuildFile; productRef = A2AE110B053B55E38F8D10C7 /* KZFileWatchers */; }; B245583C63F8F90357B87FAE /* KZFileWatchers in Frameworks */ = {isa = PBXBuildFile; productRef = A2AE110B053B55E38F8D10C7 /* KZFileWatchers */; };
B2F8E01ABA1BA30265B4ECBE /* RoundedCornerShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */; }; B2F8E01ABA1BA30265B4ECBE /* RoundedCornerShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */; };
B31E5493C99381D4E204438B /* RoomTimelineControllerFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D479DF730528153665E5782E /* RoomTimelineControllerFactoryMock.swift */; };
B3D652AA1654270742072FB3 /* DeveloperOptionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86A6F283BC574FDB96ABBB07 /* DeveloperOptionsScreenViewModel.swift */; }; B3D652AA1654270742072FB3 /* DeveloperOptionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86A6F283BC574FDB96ABBB07 /* DeveloperOptionsScreenViewModel.swift */; };
B3EDDEC1839BB5A3747624BB /* FormButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A1CCDEE545CB6453B084BF /* FormButtonStyles.swift */; }; B3EDDEC1839BB5A3747624BB /* FormButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A1CCDEE545CB6453B084BF /* FormButtonStyles.swift */; };
B402708F8728DD0DB7C324E2 /* StartChatScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */; }; B402708F8728DD0DB7C324E2 /* StartChatScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */; };
@@ -1563,7 +1563,6 @@
7061BE2C0BF427C38AEDEF5E /* SecureBackupRecoveryKeyScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModel.swift; sourceTree = "<group>"; }; 7061BE2C0BF427C38AEDEF5E /* SecureBackupRecoveryKeyScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModel.swift; sourceTree = "<group>"; };
70C86696AC9521F8ED88FBEB /* MediaUploadPreviewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreen.swift; sourceTree = "<group>"; }; 70C86696AC9521F8ED88FBEB /* MediaUploadPreviewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreen.swift; sourceTree = "<group>"; };
713B48DBF65DE4B0DD445D66 /* ReportContentScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 713B48DBF65DE4B0DD445D66 /* ReportContentScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenViewModelProtocol.swift; sourceTree = "<group>"; };
71556206CD5E8B1F53F07178 /* MockRoomTimelineControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineControllerFactory.swift; sourceTree = "<group>"; };
718D8767035D37E2DB5CC550 /* QRCodeLoginScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 718D8767035D37E2DB5CC550 /* QRCodeLoginScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenViewModelProtocol.swift; sourceTree = "<group>"; };
7199693797B66245EF97BCF5 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 7199693797B66245EF97BCF5 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
71A7D4DDEEE5D2CA0C8D63CD /* SoftLogoutScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreen.swift; sourceTree = "<group>"; }; 71A7D4DDEEE5D2CA0C8D63CD /* SoftLogoutScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftLogoutScreen.swift; sourceTree = "<group>"; };
@@ -1967,6 +1966,7 @@
D3F219838588C62198E726E3 /* LABiometryType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LABiometryType.swift; sourceTree = "<group>"; }; D3F219838588C62198E726E3 /* LABiometryType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LABiometryType.swift; sourceTree = "<group>"; };
D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreen.swift; sourceTree = "<group>"; }; D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreen.swift; sourceTree = "<group>"; };
D45C9EAA86423D7D3126DE4F /* VoiceMessageRecorderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecorderProtocol.swift; sourceTree = "<group>"; }; D45C9EAA86423D7D3126DE4F /* VoiceMessageRecorderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecorderProtocol.swift; sourceTree = "<group>"; };
D479DF730528153665E5782E /* RoomTimelineControllerFactoryMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactoryMock.swift; sourceTree = "<group>"; };
D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = "<group>"; }; D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = "<group>"; };
D4DA544B2520BFA65D6DB4BB /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; }; D4DA544B2520BFA65D6DB4BB /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
D529B976F8B2AA654D923422 /* VoiceMessageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomTimelineItem.swift; sourceTree = "<group>"; }; D529B976F8B2AA654D923422 /* VoiceMessageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomTimelineItem.swift; sourceTree = "<group>"; };
@@ -2650,7 +2650,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
52D7074991B3267B26D89B22 /* MockRoomTimelineController.swift */, 52D7074991B3267B26D89B22 /* MockRoomTimelineController.swift */,
71556206CD5E8B1F53F07178 /* MockRoomTimelineControllerFactory.swift */,
9F85164F9475FF2867F71AAA /* RoomTimelineController.swift */, 9F85164F9475FF2867F71AAA /* RoomTimelineController.swift */,
E6FCC416A3BFE73DF7B3E6BF /* RoomTimelineControllerFactory.swift */, E6FCC416A3BFE73DF7B3E6BF /* RoomTimelineControllerFactory.swift */,
18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */, 18FE0CDF1FFA92EA7EE17B0B /* RoomTimelineControllerFactoryProtocol.swift */,
@@ -2681,6 +2680,7 @@
F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */, F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */,
1ABDE6F66532CBEB0E016F94 /* RoomProxyMock.swift */, 1ABDE6F66532CBEB0E016F94 /* RoomProxyMock.swift */,
FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */, FC83F47D2173B7538AA72E0E /* RoomSummaryProviderMock.swift */,
D479DF730528153665E5782E /* RoomTimelineControllerFactoryMock.swift */,
248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */, 248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */,
7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */, 7893780A1FD6E3F38B3E9049 /* UserIndicatorControllerMock.swift */,
AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */, AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */,
@@ -6160,7 +6160,6 @@
152AE2B8650FB23AFD2E28B9 /* MockAuthenticationServiceProxy.swift in Sources */, 152AE2B8650FB23AFD2E28B9 /* MockAuthenticationServiceProxy.swift in Sources */,
B659E3A49889E749E3239EA7 /* MockMediaProvider.swift in Sources */, B659E3A49889E749E3239EA7 /* MockMediaProvider.swift in Sources */,
09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */, 09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */,
158A2D528CC78C4E7A8ED608 /* MockRoomTimelineControllerFactory.swift in Sources */,
B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */, B721125D17A0BA86794F29FB /* MockServerSelectionScreenState.swift in Sources */,
AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */, AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */,
D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */, D8359F67AF3A83516E9083C1 /* MockUserSession.swift in Sources */,
@@ -6367,6 +6366,7 @@
AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */, AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */,
2ABF11717C64054CEF2819A3 /* RoomTimelineController.swift in Sources */, 2ABF11717C64054CEF2819A3 /* RoomTimelineController.swift in Sources */,
38896D54D6D675534E606195 /* RoomTimelineControllerFactory.swift in Sources */, 38896D54D6D675534E606195 /* RoomTimelineControllerFactory.swift in Sources */,
B31E5493C99381D4E204438B /* RoomTimelineControllerFactoryMock.swift in Sources */,
7ECF12D5DCD69F67BD3E3842 /* RoomTimelineControllerFactoryProtocol.swift in Sources */, 7ECF12D5DCD69F67BD3E3842 /* RoomTimelineControllerFactoryProtocol.swift in Sources */,
E89536FC8C0E4B79E9842A78 /* RoomTimelineControllerProtocol.swift in Sources */, E89536FC8C0E4B79E9842A78 /* RoomTimelineControllerProtocol.swift in Sources */,
4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */, 4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */,
@@ -379,9 +379,11 @@ private struct NavigationSplitCoordinatorView: View {
// through the NavigationSplitCoordinator as well. // through the NavigationSplitCoordinator as well.
.sheet(item: $navigationSplitCoordinator.sheetModule) { module in .sheet(item: $navigationSplitCoordinator.sheetModule) { module in
module.coordinator?.toPresentable() module.coordinator?.toPresentable()
.id(module.id)
} }
.fullScreenCover(item: $navigationSplitCoordinator.fullScreenCoverModule) { module in .fullScreenCover(item: $navigationSplitCoordinator.fullScreenCoverModule) { module in
module.coordinator?.toPresentable() module.coordinator?.toPresentable()
.id(module.id)
} }
// Handle `horizontalSizeClass` changes breaking the navigation bar // Handle `horizontalSizeClass` changes breaking the navigation bar
// https://github.com/element-hq/element-x-ios/issues/617 // https://github.com/element-hq/element-x-ios/issues/617
@@ -408,8 +410,10 @@ private struct NavigationSplitCoordinatorView: View {
var navigationStack: some View { var navigationStack: some View {
NavigationStack(path: navigationSplitCoordinator.compactLayoutStackModulesBinding) { NavigationStack(path: navigationSplitCoordinator.compactLayoutStackModulesBinding) {
navigationSplitCoordinator.compactLayoutRootModule?.coordinator?.toPresentable() navigationSplitCoordinator.compactLayoutRootModule?.coordinator?.toPresentable()
.id(navigationSplitCoordinator.compactLayoutRootModule?.id) // Is a nil ID ok?
.navigationDestination(for: NavigationModule.self) { module in .navigationDestination(for: NavigationModule.self) { module in
module.coordinator?.toPresentable() module.coordinator?.toPresentable()
.id(module.id)
} }
} }
} }
@@ -419,19 +423,24 @@ private struct NavigationSplitCoordinatorView: View {
NavigationSplitView(columnVisibility: $columnVisibility) { NavigationSplitView(columnVisibility: $columnVisibility) {
if let sidebarModule = navigationSplitCoordinator.sidebarModule { if let sidebarModule = navigationSplitCoordinator.sidebarModule {
sidebarModule.coordinator?.toPresentable() sidebarModule.coordinator?.toPresentable()
.id(sidebarModule.id)
} else { } else {
navigationSplitCoordinator.placeholderModule.coordinator?.toPresentable() navigationSplitCoordinator.placeholderModule.coordinator?.toPresentable()
.id(navigationSplitCoordinator.placeholderModule.id)
} }
} detail: { } detail: {
if let detailModule = navigationSplitCoordinator.detailModule { if let detailModule = navigationSplitCoordinator.detailModule {
detailModule.coordinator?.toPresentable() detailModule.coordinator?.toPresentable()
.id(detailModule.id)
} else { } else {
navigationSplitCoordinator.placeholderModule.coordinator?.toPresentable() navigationSplitCoordinator.placeholderModule.coordinator?.toPresentable()
.id(navigationSplitCoordinator.placeholderModule.id)
} }
} }
.navigationSplitViewStyle(.balanced) .navigationSplitViewStyle(.balanced)
.navigationDestination(for: NavigationModule.self) { module in .navigationDestination(for: NavigationModule.self) { module in
module.coordinator?.toPresentable() module.coordinator?.toPresentable()
.id(module.id)
} }
.animation(.elementDefault, value: navigationSplitCoordinator.sidebarModule) .animation(.elementDefault, value: navigationSplitCoordinator.sidebarModule)
.animation(.elementDefault, value: navigationSplitCoordinator.detailModule) .animation(.elementDefault, value: navigationSplitCoordinator.detailModule)
@@ -713,15 +722,19 @@ private struct NavigationStackCoordinatorView: View {
var body: some View { var body: some View {
NavigationStack(path: $navigationStackCoordinator.stackModules) { NavigationStack(path: $navigationStackCoordinator.stackModules) {
navigationStackCoordinator.rootModule?.coordinator?.toPresentable() navigationStackCoordinator.rootModule?.coordinator?.toPresentable()
.id(navigationStackCoordinator.rootModule?.id) // Is a nil ID ok?
.navigationDestination(for: NavigationModule.self) { module in .navigationDestination(for: NavigationModule.self) { module in
module.coordinator?.toPresentable() module.coordinator?.toPresentable()
.id(module.id)
} }
} }
.sheet(item: $navigationStackCoordinator.sheetModule) { module in .sheet(item: $navigationStackCoordinator.sheetModule) { module in
module.coordinator?.toPresentable() module.coordinator?.toPresentable()
.id(module.id)
} }
.fullScreenCover(item: $navigationStackCoordinator.fullScreenCoverModule) { module in .fullScreenCover(item: $navigationStackCoordinator.fullScreenCoverModule) { module in
module.coordinator?.toPresentable() module.coordinator?.toPresentable()
.id(module.id)
} }
.animation(.elementDefault, value: navigationStackCoordinator.rootModule) .animation(.elementDefault, value: navigationStackCoordinator.rootModule)
} }
@@ -47,6 +47,11 @@ enum RoomFlowCoordinatorEntryPoint: Hashable {
case eventID(String) case eventID(String)
/// The flow will start by showing the room's details. /// The flow will start by showing the room's details.
case roomDetails case roomDetails
var isEventID: Bool {
guard case .eventID = self else { return false }
return true
}
} }
// swiftlint:disable:next type_body_length // swiftlint:disable:next type_body_length
@@ -508,7 +513,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .initial, .roomDetails(isRoot: true), .joinRoomScreen: case .initial, .roomDetails(isRoot: true), .joinRoomScreen:
break break
default: default:
return // The room is already on the stack, no need to present it again // The room is already on the stack, no need to present it again
// Check if we need to focus on an event
if let focussedEventID {
roomScreenCoordinator?.focusOnEvent(eventID: focussedEventID)
}
return
} }
} }
@@ -524,6 +536,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy, let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy,
initialFocussedEventID: focussedEventID,
timelineItemFactory: timelineItemFactory) timelineItemFactory: timelineItemFactory)
self.timelineController = timelineController self.timelineController = timelineController
@@ -1012,6 +1025,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
let roomTimelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy, let roomTimelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy,
initialFocussedEventID: nil,
timelineItemFactory: timelineItemFactory) timelineItemFactory: timelineItemFactory)
let parameters = RoomPollsHistoryScreenCoordinatorParameters(roomProxy: roomProxy, let parameters = RoomPollsHistoryScreenCoordinatorParameters(roomProxy: roomProxy,
@@ -272,15 +272,15 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
presentHomeScreen() presentHomeScreen()
attemptStartingOnboarding() attemptStartingOnboarding()
case(.roomList(let selectedRoomID), .selectRoom(let roomID, let entryPoint), .roomList): case(.roomList(let selectedRoomID), .selectRoom(let roomID, let entryPoint), .roomList):
if selectedRoomID == roomID { if selectedRoomID == roomID,
if let roomFlowCoordinator { !entryPoint.isEventID, // Don't reuse the existing room so the live timeline is hidden while the detached timeline is loading.
let route: AppRoute = switch entryPoint { let roomFlowCoordinator {
case .room: .room(roomID: roomID) let route: AppRoute = switch entryPoint {
case .eventID(let eventID): .event(roomID: roomID, eventID: eventID) case .room: .room(roomID: roomID)
case .roomDetails: .roomDetails(roomID: roomID) case .roomDetails: .roomDetails(roomID: roomID)
} case .eventID(let eventID): .event(roomID: roomID, eventID: eventID) // ignored.
roomFlowCoordinator.handleAppRoute(route, animated: animated)
} }
roomFlowCoordinator.handleAppRoute(route, animated: animated)
} else { } else {
Task { await self.startRoomFlow(roomID: roomID, entryPoint: entryPoint, animated: animated) } Task { await self.startRoomFlow(roomID: roomID, entryPoint: entryPoint, animated: animated) }
} }
@@ -9652,6 +9652,77 @@ class RoomSummaryProviderMock: RoomSummaryProviderProtocol {
setFilterClosure?(filter) setFilterClosure?(filter)
} }
} }
class RoomTimelineControllerFactoryMock: RoomTimelineControllerFactoryProtocol {
//MARK: - buildRoomTimelineController
var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = 0
var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount: Int {
get {
if Thread.isMainThread {
return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingCallsCount = newValue
}
}
}
}
var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCalled: Bool {
return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount > 0
}
var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments: (roomProxy: RoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol)?
var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedInvocations: [(roomProxy: RoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol)] = []
var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue: RoomTimelineControllerProtocol!
var buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue: RoomTimelineControllerProtocol! {
get {
if Thread.isMainThread {
return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryUnderlyingReturnValue
} else {
var returnValue: RoomTimelineControllerProtocol? = nil
DispatchQueue.main.sync {
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)
buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedInvocations.append((roomProxy: roomProxy, initialFocussedEventID: initialFocussedEventID, timelineItemFactory: timelineItemFactory))
if let buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure = buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure {
return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryClosure(roomProxy, initialFocussedEventID, timelineItemFactory)
} else {
return buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue
}
}
}
class RoomTimelineProviderMock: RoomTimelineProviderProtocol { class RoomTimelineProviderMock: RoomTimelineProviderProtocol {
var updatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never> { var updatePublisher: AnyPublisher<([TimelineItemProxy], PaginationState), Never> {
get { return underlyingUpdatePublisher } get { return underlyingUpdatePublisher }
@@ -16,11 +16,18 @@
import Foundation import Foundation
struct MockRoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol { struct RoomTimelineControllerFactoryMockConfiguration {
func buildRoomTimelineController(roomProxy: RoomProxyProtocol, var timelineController: MockRoomTimelineController?
timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol { }
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk extension RoomTimelineControllerFactoryMock {
return timelineController convenience init(configuration: RoomTimelineControllerFactoryMockConfiguration) {
self.init()
buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReturnValue = configuration.timelineController ?? {
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk
return timelineController
}()
} }
} }
@@ -62,6 +62,7 @@ struct RoomDetailsScreen: View {
} }
} }
.navigationTitle(L10n.screenRoomDetailsTitle) .navigationTitle(L10n.screenRoomDetailsTitle)
.navigationBarTitleDisplayMode(.inline)
.track(screen: .RoomDetails) .track(screen: .RoomDetails)
.interactiveQuickLook(item: $context.mediaPreviewItem, shouldHideControls: true) .interactiveQuickLook(item: $context.mediaPreviewItem, shouldHideControls: true)
} }
@@ -95,28 +95,28 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
bindings: .init(reactionsCollapsed: [:])), bindings: .init(reactionsCollapsed: [:])),
imageProvider: mediaProvider) imageProvider: mediaProvider)
// This may change to load the detached timeline directly. if focussedEventID != nil {
if let focussedEventID { // The timeline controller will start loading a detached timeline.
Task { await focusOnEvent(eventID: focussedEventID) } showFocusLoadingIndicator()
} }
setupSubscriptions() setupSubscriptions()
setupDirectRoomSubscriptionsIfNeeded() setupDirectRoomSubscriptionsIfNeeded()
state.timelineItemMenuActionProvider = { [weak self] itemId -> TimelineItemMenuActions? in state.timelineItemMenuActionProvider = { [weak self] itemID -> TimelineItemMenuActions? in
guard let self else { guard let self else {
return nil return nil
} }
return self.roomScreenInteractionHandler.timelineItemMenuActionsForItemId(itemId) return self.roomScreenInteractionHandler.timelineItemMenuActionsForItemId(itemID)
} }
state.audioPlayerStateProvider = { [weak self] itemId -> AudioPlayerState? in state.audioPlayerStateProvider = { [weak self] itemID -> AudioPlayerState? in
guard let self else { guard let self else {
return nil return nil
} }
return self.roomScreenInteractionHandler.audioPlayerState(for: itemId) return self.roomScreenInteractionHandler.audioPlayerState(for: itemID)
} }
buildTimelineViews() buildTimelineViews()
@@ -191,8 +191,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
case .focusLive: case .focusLive:
focusLive() focusLive()
case .scrolledToFocussedItem: case .scrolledToFocussedItem:
// Use a Task to mutate view state after the current view update. Task { // Use a Task to mutate view state after the current view update.
Task { state.timelineViewState.focussedEventNeedsDisplay = false } state.timelineViewState.focussedEventNeedsDisplay = false
hideFocusLoadingIndicator()
}
} }
} }
@@ -44,6 +44,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
} }
init(roomProxy: RoomProxyProtocol, init(roomProxy: RoomProxyProtocol,
initialFocussedEventID: String?,
timelineItemFactory: RoomTimelineItemFactoryProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol,
appSettings: AppSettings) { appSettings: AppSettings) {
self.roomProxy = roomProxy self.roomProxy = roomProxy
@@ -54,9 +55,24 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
activeTimeline = roomProxy.timeline activeTimeline = roomProxy.timeline
activeTimelineProvider = liveTimelineProvider activeTimelineProvider = liveTimelineProvider
configureActiveTimelineProvider()
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
guard let initialFocussedEventID else {
configureActiveTimelineProvider()
return
}
Task {
callbacks.send(.paginationState(PaginationState(backward: .paginating, forward: .paginating)))
switch await focusOnEvent(initialFocussedEventID, timelineSize: 100) {
case .success:
break
case .failure:
// Setup the live timeline as a fallback.
configureActiveTimelineProvider()
}
}
} }
func focusOnEvent(_ eventID: String, timelineSize: UInt16) async -> Result<Void, RoomTimelineControllerError> { func focusOnEvent(_ eventID: String, timelineSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
@@ -78,7 +94,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
func focusLive() { func focusLive() {
activeTimeline = roomProxy.timeline activeTimeline = roomProxy.timeline
activeTimelineProvider = liveTimelineProvider activeTimelineProvider = liveTimelineProvider
callbacks.send(.isLive(true))
} }
func paginateBackwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError> { func paginateBackwards(requestSize: UInt16) async -> Result<Void, RoomTimelineControllerError> {
@@ -18,8 +18,10 @@ import Foundation
struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol { struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
func buildRoomTimelineController(roomProxy: RoomProxyProtocol, func buildRoomTimelineController(roomProxy: RoomProxyProtocol,
initialFocussedEventID: String?,
timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol { timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol {
RoomTimelineController(roomProxy: roomProxy, RoomTimelineController(roomProxy: roomProxy,
initialFocussedEventID: initialFocussedEventID,
timelineItemFactory: timelineItemFactory, timelineItemFactory: timelineItemFactory,
appSettings: ServiceLocator.shared.settings) appSettings: ServiceLocator.shared.settings)
} }
@@ -19,5 +19,9 @@ import Foundation
@MainActor @MainActor
protocol RoomTimelineControllerFactoryProtocol { protocol RoomTimelineControllerFactoryProtocol {
func buildRoomTimelineController(roomProxy: RoomProxyProtocol, func buildRoomTimelineController(roomProxy: RoomProxyProtocol,
initialFocussedEventID: String?,
timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol
} }
// sourcery: AutoMockable
extension RoomTimelineControllerFactoryProtocol { }
@@ -503,7 +503,7 @@ class MockScreen: Identifiable {
appLockService: AppLockService(keychainController: KeychainControllerMock(), appLockService: AppLockService(keychainController: KeychainControllerMock(),
appSettings: ServiceLocator.shared.settings), appSettings: ServiceLocator.shared.settings),
bugReportService: BugReportServiceMock(), bugReportService: BugReportServiceMock(),
roomTimelineControllerFactory: MockRoomTimelineControllerFactory(), roomTimelineControllerFactory: RoomTimelineControllerFactoryMock(configuration: .init()),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: appSettings, appSettings: appSettings,
analytics: ServiceLocator.shared.analytics, analytics: ServiceLocator.shared.analytics,
@@ -22,6 +22,7 @@ import Combine
@MainActor @MainActor
class RoomFlowCoordinatorTests: XCTestCase { class RoomFlowCoordinatorTests: XCTestCase {
var clientProxy: ClientProxyMock! var clientProxy: ClientProxyMock!
var timelineControllerFactory: RoomTimelineControllerFactoryMock!
var roomFlowCoordinator: RoomFlowCoordinator! var roomFlowCoordinator: RoomFlowCoordinator!
var navigationStackCoordinator: NavigationStackCoordinator! var navigationStackCoordinator: NavigationStackCoordinator!
var cancellables = Set<AnyCancellable>() var cancellables = Set<AnyCancellable>()
@@ -216,7 +217,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator) XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0) XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
try await process(route: .event(roomID: "1", eventID: "2")) try await process(route: .childEvent(roomID: "1", eventID: "2"))
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator) XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0) XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
@@ -269,6 +270,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
private func setupRoomFlowCoordinator(asChildFlow: Bool = false, roomType: RoomType? = nil) async { private func setupRoomFlowCoordinator(asChildFlow: Bool = false, roomType: RoomType? = nil) async {
cancellables.removeAll() cancellables.removeAll()
clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))))) clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
timelineControllerFactory = RoomTimelineControllerFactoryMock(configuration: .init())
clientProxy.roomPreviewForIdentifierClosure = { [roomType] roomID in clientProxy.roomPreviewForIdentifierClosure = { [roomType] roomID in
switch roomType { switch roomType {
@@ -309,7 +311,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
roomFlowCoordinator = await RoomFlowCoordinator(roomID: roomID, roomFlowCoordinator = await RoomFlowCoordinator(roomID: roomID,
userSession: userSession, userSession: userSession,
isChildFlow: asChildFlow, isChildFlow: asChildFlow,
roomTimelineControllerFactory: MockRoomTimelineControllerFactory(), roomTimelineControllerFactory: timelineControllerFactory,
navigationStackCoordinator: navigationStackCoordinator, navigationStackCoordinator: navigationStackCoordinator,
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
@@ -172,6 +172,7 @@ class RoomScreenViewModelTests: XCTestCase {
XCTAssertEqual(timelineController.focusOnEventCallCount, 0) XCTAssertEqual(timelineController.focusOnEventCallCount, 0)
XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive) XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive)
XCTAssertNil(viewModel.context.viewState.timelineViewState.focussedEventID) XCTAssertNil(viewModel.context.viewState.timelineViewState.focussedEventID)
XCTAssertFalse(viewModel.context.viewState.timelineViewState.focussedEventNeedsDisplay)
// When focussing on an item that isn't loaded. // When focussing on an item that isn't loaded.
let deferred = deferFulfillment(viewModel.context.$viewState) { !$0.timelineViewState.isLive } let deferred = deferFulfillment(viewModel.context.$viewState) { !$0.timelineViewState.isLive }
@@ -182,6 +183,7 @@ class RoomScreenViewModelTests: XCTestCase {
XCTAssertEqual(timelineController.focusOnEventCallCount, 1) XCTAssertEqual(timelineController.focusOnEventCallCount, 1)
XCTAssertFalse(viewModel.context.viewState.timelineViewState.isLive) XCTAssertFalse(viewModel.context.viewState.timelineViewState.isLive)
XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEventID, "t4") XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEventID, "t4")
XCTAssertTrue(viewModel.context.viewState.timelineViewState.focussedEventNeedsDisplay)
} }
func testFocusLoadedItem() async throws { func testFocusLoadedItem() async throws {
@@ -196,6 +198,7 @@ class RoomScreenViewModelTests: XCTestCase {
XCTAssertEqual(timelineController.focusOnEventCallCount, 0) XCTAssertEqual(timelineController.focusOnEventCallCount, 0)
XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive) XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive)
XCTAssertNil(viewModel.context.viewState.timelineViewState.focussedEventID) XCTAssertNil(viewModel.context.viewState.timelineViewState.focussedEventID)
XCTAssertFalse(viewModel.context.viewState.timelineViewState.focussedEventNeedsDisplay)
// When focussing on a loaded item. // When focussing on a loaded item.
let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { !$0.timelineViewState.isLive } let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { !$0.timelineViewState.isLive }
@@ -206,6 +209,7 @@ class RoomScreenViewModelTests: XCTestCase {
XCTAssertEqual(timelineController.focusOnEventCallCount, 0) XCTAssertEqual(timelineController.focusOnEventCallCount, 0)
XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive) XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive)
XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEventID, "t1") XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEventID, "t1")
XCTAssertTrue(viewModel.context.viewState.timelineViewState.focussedEventNeedsDisplay)
} }
func testFocusLive() async throws { func testFocusLive() async throws {
@@ -225,6 +229,7 @@ class RoomScreenViewModelTests: XCTestCase {
XCTAssertEqual(timelineController.focusLiveCallCount, 0) XCTAssertEqual(timelineController.focusLiveCallCount, 0)
XCTAssertFalse(viewModel.context.viewState.timelineViewState.isLive) XCTAssertFalse(viewModel.context.viewState.timelineViewState.isLive)
XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEventID, "t4") XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEventID, "t4")
XCTAssertTrue(viewModel.context.viewState.timelineViewState.focussedEventNeedsDisplay)
// When switching back to a live timeline. // When switching back to a live timeline.
deferred = deferFulfillment(viewModel.context.$viewState) { $0.timelineViewState.isLive } deferred = deferFulfillment(viewModel.context.$viewState) { $0.timelineViewState.isLive }
@@ -235,6 +240,15 @@ class RoomScreenViewModelTests: XCTestCase {
XCTAssertEqual(timelineController.focusLiveCallCount, 1) XCTAssertEqual(timelineController.focusLiveCallCount, 1)
XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive) XCTAssertTrue(viewModel.context.viewState.timelineViewState.isLive)
XCTAssertNil(viewModel.context.viewState.timelineViewState.focussedEventID) XCTAssertNil(viewModel.context.viewState.timelineViewState.focussedEventID)
XCTAssertFalse(viewModel.context.viewState.timelineViewState.focussedEventNeedsDisplay)
}
func testInitialFocusViewState() async throws {
let timelineController = MockRoomTimelineController()
let viewModel = makeViewModel(focussedEventID: "t10", timelineController: timelineController)
XCTAssertEqual(viewModel.context.viewState.timelineViewState.focussedEventID, "t10")
XCTAssertTrue(viewModel.context.viewState.timelineViewState.focussedEventNeedsDisplay)
} }
// MARK: - Sending // MARK: - Sending
@@ -464,8 +478,11 @@ class RoomScreenViewModelTests: XCTestCase {
// MARK: - Helpers // MARK: - Helpers
private func makeViewModel(roomProxy: RoomProxyProtocol? = nil, timelineController: RoomTimelineControllerProtocol) -> RoomScreenViewModel { private func makeViewModel(roomProxy: RoomProxyProtocol? = nil,
focussedEventID: String? = nil,
timelineController: RoomTimelineControllerProtocol) -> RoomScreenViewModel {
RoomScreenViewModel(roomProxy: roomProxy ?? RoomProxyMock(with: .init(name: "")), RoomScreenViewModel(roomProxy: roomProxy ?? RoomProxyMock(with: .init(name: "")),
focussedEventID: focussedEventID,
timelineController: timelineController, timelineController: timelineController,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
@@ -22,6 +22,7 @@ import Combine
@MainActor @MainActor
class UserSessionFlowCoordinatorTests: XCTestCase { class UserSessionFlowCoordinatorTests: XCTestCase {
var clientProxy: ClientProxyMock! var clientProxy: ClientProxyMock!
var timelineControllerFactory: RoomTimelineControllerFactoryMock!
var userSessionFlowCoordinator: UserSessionFlowCoordinator! var userSessionFlowCoordinator: UserSessionFlowCoordinator!
var navigationRootCoordinator: NavigationRootCoordinator! var navigationRootCoordinator: NavigationRootCoordinator!
var notificationManager: NotificationManagerMock! var notificationManager: NotificationManagerMock!
@@ -35,6 +36,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
override func setUp() async throws { override func setUp() async throws {
cancellables.removeAll() cancellables.removeAll()
clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))))) clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
timelineControllerFactory = RoomTimelineControllerFactoryMock(configuration: .init())
let mediaProvider = MockMediaProvider() let mediaProvider = MockMediaProvider()
let voiceMessageMediaManager = VoiceMessageMediaManagerMock() let voiceMessageMediaManager = VoiceMessageMediaManagerMock()
let userSession = MockUserSession(clientProxy: clientProxy, let userSession = MockUserSession(clientProxy: clientProxy,
@@ -49,7 +51,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
navigationRootCoordinator: navigationRootCoordinator, navigationRootCoordinator: navigationRootCoordinator,
appLockService: AppLockServiceMock(), appLockService: AppLockServiceMock(),
bugReportService: BugReportServiceMock(), bugReportService: BugReportServiceMock(),
roomTimelineControllerFactory: MockRoomTimelineControllerFactory(), roomTimelineControllerFactory: timelineControllerFactory,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics, analytics: ServiceLocator.shared.analytics,
@@ -216,22 +218,40 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
} }
func testEventRoutes() async throws { func testEventRoutes() async throws {
// A regular event route should set its room as the root of the stack and focus on the event.
try await process(route: .event(roomID: "1", eventID: "1"), expectedState: .roomList(selectedRoomID: "1")) try await process(route: .event(roomID: "1", eventID: "1"), expectedState: .roomList(selectedRoomID: "1"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0)
XCTAssertNotNil(detailCoordinator) XCTAssertNotNil(detailCoordinator)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount, 1)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments?.initialFocussedEventID, "1")
// A child event route should push a new room screen onto the stack and focus on the event.
userSessionFlowCoordinator.handleAppRoute(.childEvent(roomID: "2", eventID: "2"), animated: true) userSessionFlowCoordinator.handleAppRoute(.childEvent(roomID: "2", eventID: "2"), animated: true)
try await Task.sleep(for: .milliseconds(100)) try await Task.sleep(for: .milliseconds(100))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 1) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 1)
XCTAssertTrue(detailNavigationStack?.stackCoordinators.first is RoomScreenCoordinator) XCTAssertTrue(detailNavigationStack?.stackCoordinators.first is RoomScreenCoordinator)
XCTAssertNotNil(detailCoordinator) XCTAssertNotNil(detailCoordinator)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount, 2)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments?.initialFocussedEventID, "2")
// A subsequent regular event route should clear the stack and set the new room as the root of the stack.
try await process(route: .event(roomID: "3", eventID: "3"), expectedState: .roomList(selectedRoomID: "3")) try await process(route: .event(roomID: "3", eventID: "3"), expectedState: .roomList(selectedRoomID: "3"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0)
XCTAssertNotNil(detailCoordinator) XCTAssertNotNil(detailCoordinator)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount, 3)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments?.initialFocussedEventID, "3")
// A regular event route for the same room should set a new instance of the room as the root of the stack.
try await process(route: .event(roomID: "3", eventID: "4"), expectedState: .roomList(selectedRoomID: "3"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0)
XCTAssertNotNil(detailCoordinator)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryCallsCount, 4)
XCTAssertEqual(timelineControllerFactory.buildRoomTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryReceivedArguments?.initialFocussedEventID, "4",
"A new timeline should be created for the same room ID, so that the screen isn't stale while loading.")
} }
// MARK: - Private // MARK: - Private