Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flow coordinators #927

Merged
merged 2 commits into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions ElementX/Sources/Application/FlowCoordinatorProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation

@MainActor
protocol FlowCoordinatorProtocol {
stefanceriu marked this conversation as resolved.
Show resolved Hide resolved
func handleAppRoute(_ appRoute: AppRoute, animated: Bool)
stefanceriu marked this conversation as resolved.
Show resolved Hide resolved
}
2 changes: 2 additions & 0 deletions ElementX/Sources/Application/Navigation/AppRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import Foundation
import URLRouting

enum AppRoute {
case roomList
case room(roomID: String)
case roomDetails(roomID: String)
}

struct AppRouterManager {
Expand Down
480 changes: 480 additions & 0 deletions ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ enum UserSessionFlowCoordinatorAction {
case clearCache
}

class UserSessionFlowCoordinator: CoordinatorProtocol {
private let stateMachine: UserSessionFlowCoordinatorStateMachine
private var cancellables: Set<AnyCancellable> = .init()

class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
private let userSession: UserSessionProtocol
private let navigationSplitCoordinator: NavigationSplitCoordinator
private let bugReportService: BugReportServiceProtocol
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
private let emojiProvider: EmojiProviderProtocol = EmojiProvider()

private let stateMachine: UserSessionFlowCoordinatorStateMachine
private let roomFlowCoordinator: RoomFlowCoordinator

private var cancellables: Set<AnyCancellable> = .init()

private let sidebarNavigationStackCoordinator: NavigationStackCoordinator
private let detailNavigationStackCoordinator: NavigationStackCoordinator
Expand All @@ -52,7 +53,23 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {

navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator)

roomFlowCoordinator = RoomFlowCoordinator(userSession: userSession,
roomTimelineControllerFactory: roomTimelineControllerFactory,
navigationStackCoordinator: detailNavigationStackCoordinator,
navigationSplitCoordinator: navigationSplitCoordinator,
emojiProvider: EmojiProvider())

setupStateMachine()

roomFlowCoordinator.actions.sink { action in
switch action {
case .presentedRoom(let roomID):
self.stateMachine.processEvent(.selectRoom(roomId: roomID))
case .dismissedRoom:
self.stateMachine.processEvent(.deselectRoom)
}
}
.store(in: &cancellables)
}

func start() {
Expand All @@ -64,6 +81,8 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
func isDisplayingRoomScreen(withRoomId roomId: String) -> Bool {
stateMachine.isDisplayingRoomScreen(withRoomId: roomId)
}

// MARK: - FlowCoordinatorProtocol

func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
switch stateMachine.state {
Expand All @@ -72,9 +91,10 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
case .roomList, .initial:
break
}

switch appRoute {
case .room(let roomID):
stateMachine.processEvent(.selectRoom(roomId: roomID), userInfo: .init(animated: animated))
case .room, .roomDetails, .roomList:
roomFlowCoordinator.handleAppRoute(appRoute, animated: true)
}
}

Expand All @@ -84,20 +104,20 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
private func setupStateMachine() {
stateMachine.addTransitionHandler { [weak self] context in
guard let self else { return }
let animated = (context.userInfo as? EventUserInfo)?.animated ?? true
let animated = (context.userInfo as? UserSessionFlowCoordinatorStateMachine.EventUserInfo)?.animated ?? true
switch (context.fromState, context.event, context.toState) {
case (.initial, .start, .roomList):
self.presentHomeScreen()

case(.roomList(let currentRoomId), .selectRoom, .roomList(let selectedRoomId)):
guard let selectedRoomId,
selectedRoomId != currentRoomId else {
return
}

self.presentRoomWithIdentifier(selectedRoomId, animated: animated)
case(.roomList, .selectRoom, .roomList):
break
case(.roomList, .deselectRoom, .roomList):
break

case (.invitesScreen, .selectRoom, .invitesScreen):
break
case (.invitesScreen, .deselectRoom, .invitesScreen):
break

case (.roomList, .showSessionVerificationScreen, .sessionVerificationScreen):
self.presentSessionVerification(animated: animated)
Expand All @@ -123,16 +143,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
self.presentInvitesList(animated: animated)
case (.invitesScreen, .closedInvitesScreen, .roomList):
break
case (.invitesScreen, .selectRoom(let roomId), .invitesScreen(let selectedRoomId)) where roomId == selectedRoomId:
self.presentRoomWithIdentifier(roomId, animated: animated)
case (.invitesScreen, .deselectRoom, .invitesScreen):
break

case (.roomList(let currentRoomId), .selectRoomDetails(let roomId), .roomList) where currentRoomId == roomId:
break
case (.roomList, .selectRoomDetails(let roomId), .roomList(let selectedRoomId)) where roomId == selectedRoomId:
self.presentRoomDetails(roomIdentifier: roomId, animated: animated)

default:
fatalError("Unknown transition: \(context)")
}
Expand All @@ -143,6 +154,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
}
}

// swiftlint:disable:next cyclomatic_complexity
private func presentHomeScreen() {
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
attributedStringBuilder: AttributedStringBuilder(),
Expand All @@ -154,12 +166,15 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
guard let self else { return }

switch action {
case .presentRoom(let roomIdentifier):
self.stateMachine.processEvent(.selectRoom(roomId: roomIdentifier))
case .presentRoomDetails(let roomIdentifier):
self.stateMachine.processEvent(.selectRoomDetails(roomId: roomIdentifier))
case .roomLeft(let roomIdentifier):
self.deselectRoomIfNeeded(roomIdentifier: roomIdentifier)
case .presentRoom(let roomID):
self.roomFlowCoordinator.handleAppRoute(.room(roomID: roomID), animated: true)
case .presentRoomDetails(let roomID):
self.roomFlowCoordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: true)
case .roomLeft(let roomID):
if case .roomList(selectedRoomId: let selectedRoomId) = stateMachine.state,
selectedRoomId == roomID {
self.roomFlowCoordinator.handleAppRoute(.roomList, animated: true)
}
case .presentSettingsScreen:
self.stateMachine.processEvent(.showSettingsScreen)
case .presentFeedbackScreen:
Expand All @@ -178,114 +193,6 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
sidebarNavigationStackCoordinator.setRootCoordinator(coordinator)
}

// MARK: Rooms

private func presentRoomWithIdentifier(_ roomIdentifier: String, animated: Bool = true) {
Task { @MainActor in
guard let roomProxy = await userSession.clientProxy.roomForIdentifier(roomIdentifier) else {
MXLog.error("Invalid room identifier: \(roomIdentifier)")
stateMachine.processEvent(.deselectRoom)
return
}
let userId = userSession.clientProxy.userID

let timelineItemFactory = RoomTimelineItemFactory(userID: userId,
mediaProvider: userSession.mediaProvider,
attributedStringBuilder: AttributedStringBuilder(),
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userId))

let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(userId: userId,
roomProxy: roomProxy,
timelineItemFactory: timelineItemFactory,
mediaProvider: userSession.mediaProvider)

let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: detailNavigationStackCoordinator,
roomProxy: roomProxy,
timelineController: timelineController,
mediaProvider: userSession.mediaProvider,
emojiProvider: emojiProvider,
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy))
let coordinator = RoomScreenCoordinator(parameters: parameters)
coordinator.callback = { [weak self] action in
switch action {
case .leftRoom:
self?.dismissRoom()
}
}

detailNavigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self, roomIdentifier] in
guard let self else { return }

// Move the state machine to no room selected if the room currently being dismissed
// is the same as the one selected in the state machine.
// This generally happens when popping the room screen while in a compact layout
switch self.stateMachine.state {
case
let .roomList(selectedRoomId) where selectedRoomId == roomIdentifier,
let .invitesScreen(selectedRoomId) where selectedRoomId == roomIdentifier:

self.stateMachine.processEvent(.deselectRoom)
self.detailNavigationStackCoordinator.setRootCoordinator(nil)
default:
break
}
}

if navigationSplitCoordinator.detailCoordinator == nil {
navigationSplitCoordinator.setDetailCoordinator(detailNavigationStackCoordinator, animated: animated)
}
}
}

private func deselectRoomIfNeeded(roomIdentifier: String) {
guard
case .roomList(selectedRoomId: let selectedRoomId) = stateMachine.state,
selectedRoomId == roomIdentifier
else {
return
}

stateMachine.processEvent(.deselectRoom)
navigationSplitCoordinator.setDetailCoordinator(nil)
}

private func dismissRoom() {
detailNavigationStackCoordinator.popToRoot(animated: true)
navigationSplitCoordinator.setDetailCoordinator(nil)
}

private func presentRoomDetails(roomIdentifier: String, animated: Bool = true) {
Task {
guard let roomProxy = await userSession.clientProxy.roomForIdentifier(roomIdentifier) else {
MXLog.error("Invalid room identifier: \(roomIdentifier)")
return
}

let params = RoomDetailsScreenCoordinatorParameters(navigationStackCoordinator: detailNavigationStackCoordinator,
roomProxy: roomProxy,
mediaProvider: userSession.mediaProvider,
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy))

let coordinator = RoomDetailsScreenCoordinator(parameters: params)

coordinator.callback = { [weak self] action in
switch action {
case .cancel, .leftRoom:
self?.stateMachine.processEvent(.deselectRoom)
self?.detailNavigationStackCoordinator.setRootCoordinator(nil)
}
}

detailNavigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self, roomIdentifier] in
self?.deselectRoomIfNeeded(roomIdentifier: roomIdentifier)
}

if navigationSplitCoordinator.detailCoordinator == nil {
navigationSplitCoordinator.setDetailCoordinator(detailNavigationStackCoordinator, animated: animated)
}
}
}

// MARK: Settings

private func presentSettingsScreen(animated: Bool) {
Expand Down Expand Up @@ -352,9 +259,9 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
switch action {
case .close:
self.navigationSplitCoordinator.setSheetCoordinator(nil)
case .openRoom(let identifier):
case .openRoom(let roomID):
self.navigationSplitCoordinator.setSheetCoordinator(nil)
self.stateMachine.processEvent(.selectRoom(roomId: identifier))
self.roomFlowCoordinator.handleAppRoute(.room(roomID: roomID), animated: true)
}
}
.store(in: &cancellables)
Expand Down Expand Up @@ -400,8 +307,8 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
coordinator.actions
.sink { [weak self] action in
switch action {
case .openRoom(let roomId):
self?.stateMachine.processEvent(.selectRoom(roomId: roomId))
case .openRoom(let roomID):
self?.roomFlowCoordinator.handleAppRoute(.room(roomID: roomID), animated: true)
}
}
.store(in: &cancellables)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ class UserSessionFlowCoordinatorStateMachine {
/// Showing invites list screen
case invitesScreen(selectedRoomId: String?)
}

struct EventUserInfo {
let animated: Bool
}

/// Events that can be triggered on the AppCoordinator state machine
enum Event: EventType {
Expand Down Expand Up @@ -77,9 +81,6 @@ class UserSessionFlowCoordinatorStateMachine {
case showInvitesScreen
/// The invites screen has been dismissed
case closedInvitesScreen

/// Request presentation of the settings of a specific room
case selectRoomDetails(roomId: String)
}

private let stateMachine: StateMachine<State, Event>
Expand All @@ -101,8 +102,12 @@ class UserSessionFlowCoordinatorStateMachine {
switch (event, fromState) {
case (.selectRoom(let roomId), .roomList):
return .roomList(selectedRoomId: roomId)
case (.selectRoom(let roomId), .invitesScreen):
return .invitesScreen(selectedRoomId: roomId)
case (.deselectRoom, .roomList):
return .roomList(selectedRoomId: nil)
case (.deselectRoom, .invitesScreen):
return .invitesScreen(selectedRoomId: nil)

case (.showSettingsScreen, .roomList(let selectedRoomId)):
return .settingsScreen(selectedRoomId: selectedRoomId)
Expand All @@ -128,14 +133,7 @@ class UserSessionFlowCoordinatorStateMachine {
return .invitesScreen(selectedRoomId: selectedRoomId)
case (.closedInvitesScreen, .invitesScreen(let selectedRoomId)):
return .roomList(selectedRoomId: selectedRoomId)
case (.selectRoom(let roomId), .invitesScreen):
return .invitesScreen(selectedRoomId: roomId)
case (.deselectRoom, .invitesScreen):
return .invitesScreen(selectedRoomId: nil)

case (.selectRoomDetails(let roomId), .roomList):
return .roomList(selectedRoomId: roomId)

default:
return nil
}
Expand Down Expand Up @@ -176,7 +174,3 @@ class UserSessionFlowCoordinatorStateMachine {
}
}
}

struct EventUserInfo {
let animated: Bool
}
Loading