Add Event Routes to the flows (#2743)

This commit is contained in:
Doug
2024-04-25 12:50:06 +01:00
committed by GitHub
parent 8581355372
commit 6661c16154
8 changed files with 138 additions and 52 deletions
@@ -206,6 +206,18 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
} else {
handleAppRoute(.childRoomAlias(alias))
}
case .event(let roomID, let eventID):
if isExternalURL {
handleAppRoute(route)
} else {
handleAppRoute(.childEvent(roomID: roomID, eventID: eventID))
}
case .eventOnRoomAlias(let alias, let eventID):
if isExternalURL {
handleAppRoute(route)
} else {
handleAppRoute(.childEventOnRoomAlias(alias: alias, eventID: eventID))
}
default:
break
}
@@ -24,16 +24,24 @@ enum AppRoute: Equatable {
case roomList
/// A room, shown as the root of the stack (popping any child rooms).
case room(roomID: String)
/// A room, shown as the root of the stack (popping any child rooms).
/// The same as ``room`` but using a room alias.
case roomAlias(String)
/// A room, pushed as a child of any existing rooms on the stack.
case childRoom(roomID: String)
/// A room, pushed as a child of any existing rooms on the stack.
/// The same as ``childRoom`` but using a room alias.
case childRoomAlias(String)
/// The information about a particular room.
case roomDetails(roomID: String)
/// The profile of a member within the current room.
case roomMemberDetails(userID: String)
/// An event within a room, shown as the root of the stack (popping any child rooms).
case event(roomID: String, eventID: String)
/// The same as ``event`` but using a room alias.
case eventOnRoomAlias(alias: String, eventID: String)
/// An event within a room, either within the last child on the stack or pushing a new child if needed.
case childEvent(roomID: String, eventID: String)
/// The same as ``childEvent`` but using a room alias.
case childEventOnRoomAlias(alias: String, eventID: String)
/// The profile of a matrix user (outside of a room).
case userProfile(userID: String)
/// An Element Call link generated outside of a chat room.
@@ -133,6 +141,12 @@ struct MatrixPermalinkParser: URLParser {
return .roomAlias(alias)
case .user(let id):
return .userProfile(userID: id)
case .eventOnRoomId(let roomID, let eventID):
// return .event(roomID: roomID, eventID: eventID)
return nil
case .eventOnRoomAlias(let alias, let eventID):
// return .eventOnRoomAlias(alias: alias, eventID: eventID)
return nil
default:
return nil
}
@@ -39,6 +39,16 @@ enum RoomFlowCoordinatorAction: Equatable {
}
}
/// A value that represents where the room flow will be started.
enum RoomFlowCoordinatorEntryPoint: Hashable {
/// The flow will start by showing the room directly.
case room
/// The flow will start by showing the room, focussing on the supplied event ID.
case eventID(String)
/// The flow will start by showing the room's details.
case roomDetails
}
// swiftlint:disable:next type_body_length
class RoomFlowCoordinator: FlowCoordinatorProtocol {
private let roomID: String
@@ -54,6 +64,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
private var roomProxy: RoomProxyProtocol!
private var roomScreenCoordinator: RoomScreenCoordinator?
private weak var joinRoomScreenCoordinator: JoinRoomScreenCoordinator?
// periphery:ignore - used to avoid deallocation
@@ -118,7 +129,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator {
childRoomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
} else if roomID != roomProxy.id {
stateMachine.tryEvent(.startChildFlow(roomID: roomID), userInfo: EventUserInfo(animated: animated))
stateMachine.tryEvent(.startChildFlow(roomID: roomID, entryPoint: .room), userInfo: EventUserInfo(animated: animated))
} else {
MXLog.info("Ignoring presentation of the same room as a child of this one.")
}
@@ -143,12 +154,24 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} else {
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID), userInfo: EventUserInfo(animated: animated))
}
case .roomAlias, .childRoomAlias, .roomList, .userProfile, .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
break
case .event(let roomID, let eventID):
Task { await handleRoomRoute(roomID: roomID, focussedEventID: eventID, animated: animated) }
case .childEvent(let roomID, let eventID):
if case .presentingChild = stateMachine.state, let childRoomFlowCoordinator {
childRoomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
} else if roomID != roomProxy.id {
stateMachine.tryEvent(.startChildFlow(roomID: roomID, entryPoint: .eventID(eventID)), userInfo: EventUserInfo(animated: animated))
} else {
roomScreenCoordinator?.focusOnEvent(eventID: eventID)
}
case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias:
break // These are converted to a room ID route one level above.
case .roomList, .userProfile, .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
break // These routes can't be handled.
}
}
private func handleRoomRoute(roomID: String, animated: Bool) async {
private func handleRoomRoute(roomID: String, focussedEventID: String? = nil, animated: Bool) async {
guard roomID == self.roomID else { fatalError("Navigation route doesn't belong to this room flow.") }
guard let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) else {
@@ -159,7 +182,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch roomProxy.membership {
case .joined:
await storeAndSubscribeToRoomProxy(roomProxy)
stateMachine.tryEvent(.presentRoom, userInfo: EventUserInfo(animated: animated))
stateMachine.tryEvent(.presentRoom(focussedEventID: focussedEventID), userInfo: EventUserInfo(animated: animated))
default:
stateMachine.tryEvent(.presentJoinRoomScreen, userInfo: EventUserInfo(animated: animated))
}
@@ -307,7 +330,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
// Child flow
case (_, .startChildFlow(let roomID)):
case (_, .startChildFlow(let roomID, _)):
return .presentingChild(childRoomID: roomID, previousState: fromState)
case (.presentingChild(_, let previousState), .dismissChildFlow):
return previousState
@@ -328,8 +351,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (_, .dismissJoinRoomScreen, .complete):
dismissFlow(animated: animated)
case (_, .presentRoom, .room):
Task { await self.presentRoom(fromState: context.fromState, animated: animated) }
case (_, .presentRoom(let focussedEventID), .room):
Task { await self.presentRoom(fromState: context.fromState, focussedEventID: focussedEventID, animated: animated) }
case (_, .dismissFlow, .complete):
dismissFlow(animated: animated)
@@ -438,8 +461,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
rolesAndPermissionsFlowCoordinator = nil
// Child flow
case (_, .startChildFlow(let roomID), .presentingChild):
Task { await self.startChildFlow(for: roomID) }
case (_, .startChildFlow(let roomID, let entryPoint), .presentingChild):
Task { await self.startChildFlow(for: roomID, entryPoint: entryPoint) }
case (.presentingChild, .dismissChildFlow, _):
childRoomFlowCoordinator = nil
@@ -467,8 +490,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
/// Updates the navigation stack so it displays the timeline for the given room
/// - Parameters:
/// - fromState: The state that asked for the room presentation.
/// - focussedEventID: An (optional) event ID that the timeline should be focussed around.
/// - animated: whether it should animate the transition
private func presentRoom(fromState: State, animated: Bool) async {
private func presentRoom(fromState: State, focussedEventID: String? = nil, animated: Bool) async {
// If any sheets are presented dismiss them, rely on their dismissal callbacks to transition the state machine
// through the correct states before presenting the room
navigationStackCoordinator.setSheetCoordinator(nil)
@@ -507,6 +532,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
let completionSuggestionService = CompletionSuggestionService(roomProxy: roomProxy)
let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy,
focussedEventID: focussedEventID,
timelineController: timelineController,
mediaProvider: userSession.mediaProvider,
mediaPlayerProvider: MediaPlayerProvider(),
@@ -548,6 +574,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
.store(in: &cancellables)
roomScreenCoordinator = coordinator
if !isChildFlow {
let animated = UIDevice.current.userInterfaceIdiom == .phone ? animated : false
navigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self] in
@@ -584,7 +611,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
if let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) {
await storeAndSubscribeToRoomProxy(roomProxy)
stateMachine.tryEvent(.presentRoom, userInfo: EventUserInfo(animated: animated))
stateMachine.tryEvent(.presentRoom(focussedEventID: nil), userInfo: EventUserInfo(animated: animated))
analytics.trackJoinedRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace, activeMemberCount: UInt(roomProxy.activeMembersCount))
} else {
@@ -1022,7 +1049,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .openUserProfile:
stateMachine.tryEvent(.presentUserProfile(userID: userID))
case .openDirectChat(let roomID):
stateMachine.tryEvent(.startChildFlow(roomID: roomID))
stateMachine.tryEvent(.startChildFlow(roomID: roomID, entryPoint: .room))
}
}
.store(in: &cancellables)
@@ -1045,7 +1072,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .openDirectChat(let roomID):
stateMachine.tryEvent(.startChildFlow(roomID: roomID))
stateMachine.tryEvent(.startChildFlow(roomID: roomID, entryPoint: .room))
case .dismiss:
break // Not supported when pushed.
}
@@ -1115,7 +1142,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
// We don't need to worry about passing in the room proxy as timelines are
// cached. The local echo will be visible when fetching the room by its ID.
stateMachine.tryEvent(.startChildFlow(roomID: roomID))
stateMachine.tryEvent(.startChildFlow(roomID: roomID, entryPoint: .room))
}
private func presentNotificationSettingsScreen() {
@@ -1249,7 +1276,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
// MARK: - Child Flow
private func startChildFlow(for roomID: String) async {
private func startChildFlow(for roomID: String, entryPoint: RoomFlowCoordinatorEntryPoint) async {
let coordinator = await RoomFlowCoordinator(roomID: roomID,
userSession: userSession,
isChildFlow: true,
@@ -1275,7 +1302,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
.store(in: &cancellables)
childRoomFlowCoordinator = coordinator
coordinator.handleAppRoute(.room(roomID: roomID), animated: true)
switch entryPoint {
case .room:
coordinator.handleAppRoute(.room(roomID: roomID), animated: true)
case .eventID(let eventID):
coordinator.handleAppRoute(.event(roomID: roomID, eventID: eventID), animated: true)
case .roomDetails:
coordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: true)
}
}
}
@@ -1329,7 +1363,7 @@ private extension RoomFlowCoordinator {
case presentJoinRoomScreen
case dismissJoinRoomScreen
case presentRoom
case presentRoom(focussedEventID: String?)
case dismissFlow
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
@@ -1384,7 +1418,7 @@ private extension RoomFlowCoordinator {
case dismissRolesAndPermissionsScreen
// Child room flow events
case startChildFlow(roomID: String)
case startChildFlow(roomID: String, entryPoint: RoomFlowCoordinatorEntryPoint)
case dismissChildFlow
}
}
@@ -196,39 +196,39 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
switch appRoute {
case .room(let roomID):
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .room), userInfo: .init(animated: animated))
case .roomAlias(let alias):
guard let roomID = await userSession.clientProxy.resolveRoomAlias(alias) else {
return
}
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
guard let roomID = await userSession.clientProxy.resolveRoomAlias(alias) else { return }
await asyncHandleAppRoute(.room(roomID: roomID), animated: animated)
case .childRoom(let roomID):
if let roomFlowCoordinator {
roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
} else {
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .room), userInfo: .init(animated: animated))
}
case .childRoomAlias(let alias):
guard let roomID = await userSession.clientProxy.resolveRoomAlias(alias) else {
return
}
if let roomFlowCoordinator {
roomFlowCoordinator.handleAppRoute(.childRoom(roomID: roomID), animated: animated)
} else {
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
}
guard let roomID = await userSession.clientProxy.resolveRoomAlias(alias) else { return }
await asyncHandleAppRoute(.childRoom(roomID: roomID), animated: animated)
case .roomDetails(let roomID):
if stateMachine.state.selectedRoomID == roomID {
roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
} else {
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: true), userInfo: .init(animated: animated))
stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .roomDetails), userInfo: .init(animated: animated))
}
case .roomList:
roomFlowCoordinator?.clearRoute(animated: animated)
case .roomMemberDetails:
roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
case .event(let roomID, let eventID):
stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .eventID(eventID)), userInfo: .init(animated: animated))
case .eventOnRoomAlias(let alias, let eventID):
guard let roomID = await userSession.clientProxy.resolveRoomAlias(alias) else { return }
await asyncHandleAppRoute(.event(roomID: roomID, eventID: eventID), animated: animated)
case .childEvent:
roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
case .childEventOnRoomAlias(let alias, let eventID):
guard let roomID = await userSession.clientProxy.resolveRoomAlias(alias) else { return }
await asyncHandleAppRoute(.childEvent(roomID: roomID, eventID: eventID), animated: animated)
case .userProfile(let userID):
stateMachine.processEvent(.showUserProfileScreen(userID: userID), userInfo: .init(animated: animated))
case .genericCallLink(let url):
@@ -266,14 +266,18 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
case (.initial, .start, .roomList):
presentHomeScreen()
attemptStartingOnboarding()
case(.roomList(let selectedRoomID), .selectRoom(let roomID, let showingRoomDetails), .roomList):
case(.roomList(let selectedRoomID), .selectRoom(let roomID, let entryPoint), .roomList):
if selectedRoomID == roomID {
if let roomFlowCoordinator {
roomFlowCoordinator.handleAppRoute(.room(roomID: roomID), animated: animated)
let route: AppRoute = switch entryPoint {
case .room: .room(roomID: roomID)
case .eventID(let eventID): .event(roomID: roomID, eventID: eventID)
case .roomDetails: .roomDetails(roomID: roomID)
}
roomFlowCoordinator.handleAppRoute(route, animated: animated)
}
} else {
Task { await self.startRoomFlow(roomID: roomID, showingRoomDetails: showingRoomDetails, animated: animated) }
Task { await self.startRoomFlow(roomID: roomID, entryPoint: entryPoint, animated: animated) }
}
case(.roomList, .deselectRoom, .roomList):
dismissRoomFlow(animated: animated)
@@ -442,7 +446,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
// MARK: Room Flow
private func startRoomFlow(roomID: String, showingRoomDetails: Bool, animated: Bool) async {
private func startRoomFlow(roomID: String,
entryPoint: RoomFlowCoordinatorEntryPoint,
animated: Bool) async {
let coordinator = await RoomFlowCoordinator(roomID: roomID,
userSession: userSession,
isChildFlow: false,
@@ -461,7 +467,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
case .finished:
stateMachine.processEvent(.deselectRoom)
case .presentRoom(let roomID):
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false))
stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .room))
case .presentCallScreen(let roomProxy):
presentCallScreen(roomProxy: roomProxy)
}
@@ -474,10 +480,13 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
navigationSplitCoordinator.setDetailCoordinator(detailNavigationStackCoordinator, animated: animated)
}
if showingRoomDetails {
coordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: animated)
} else {
switch entryPoint {
case .room:
coordinator.handleAppRoute(.room(roomID: roomID), animated: animated)
case .eventID(let eventID):
coordinator.handleAppRoute(.event(roomID: roomID, eventID: eventID), animated: animated)
case .roomDetails:
coordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: animated)
}
let availableInvitesCount = userSession.clientProxy.inviteSummaryProvider?.roomListPublisher.value.count ?? 0
@@ -518,7 +527,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
self.navigationSplitCoordinator.setSheetCoordinator(nil)
case .openRoom(let roomID):
self.navigationSplitCoordinator.setSheetCoordinator(nil)
self.stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false))
self.stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .room))
}
}
.store(in: &cancellables)
@@ -540,7 +549,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
.sink { [weak self] action in
switch action {
case .openRoom(let roomID):
self?.stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false))
self?.stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .room))
}
}
.store(in: &cancellables)
@@ -691,7 +700,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .openDirectChat(let roomID):
navigationSplitCoordinator.setSheetCoordinator(nil)
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false))
stateMachine.processEvent(.selectRoom(roomID: roomID, entryPoint: .room))
case .dismiss:
navigationSplitCoordinator.setSheetCoordinator(nil)
}
@@ -75,8 +75,9 @@ class UserSessionFlowCoordinatorStateMachine {
case start
/// Request presentation for a particular room
/// - Parameter roomID:the room identifier
case selectRoom(roomID: String, showingRoomDetails: Bool)
/// - Parameter roomID: The room identifier
/// - Parameter entryPoint: The starting point for the presented room.
case selectRoom(roomID: String, entryPoint: RoomFlowCoordinatorEntryPoint)
/// The room screen has been dismissed
case deselectRoom
@@ -21,6 +21,7 @@ import WysiwygComposer
struct RoomScreenCoordinatorParameters {
let roomProxy: RoomProxyProtocol
var focussedEventID: String?
let timelineController: RoomTimelineControllerProtocol
let mediaProvider: MediaProviderProtocol
let mediaPlayerProvider: MediaPlayerProviderProtocol
@@ -59,6 +60,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
init(parameters: RoomScreenCoordinatorParameters) {
viewModel = RoomScreenViewModel(roomProxy: parameters.roomProxy,
focussedEventID: parameters.focussedEventID,
timelineController: parameters.timelineController,
mediaProvider: parameters.mediaProvider,
mediaPlayerProvider: parameters.mediaPlayerProvider,
@@ -129,6 +131,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
.store(in: &cancellables)
}
func focusOnEvent(eventID: String) {
Task { await viewModel.focusOnEvent(eventID: eventID) }
}
func stop() {
viewModel.stop()
}
@@ -49,6 +49,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
private var paginateBackwardsTask: Task<Void, Never>?
init(roomProxy: RoomProxyProtocol,
focussedEventID: String? = nil,
timelineController: RoomTimelineControllerProtocol,
mediaProvider: MediaProviderProtocol,
mediaPlayerProvider: MediaPlayerProviderProtocol,
@@ -90,6 +91,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
bindings: .init(reactionsCollapsed: [:])),
imageProvider: mediaProvider)
// This may change to load the detached timeline directly.
if let focussedEventID {
Task { await focusOnEvent(eventID: focussedEventID) }
}
setupSubscriptions()
setupDirectRoomSubscriptionsIfNeeded()
@@ -208,6 +214,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
}
func focusOnEvent(eventID: String) async { }
// MARK: - Private
private func attach(_ attachment: ComposerAttachmentType) {
@@ -23,5 +23,7 @@ protocol RoomScreenViewModelProtocol {
var actions: AnyPublisher<RoomScreenViewModelAction, Never> { get }
var context: RoomScreenViewModelType.Context { get }
func process(composerAction: ComposerToolbarViewModelAction)
/// Updates the timeline to show and highlight the item with the corresponding event ID.
func focusOnEvent(eventID: String) async
func stop()
}