Pinned Events Timeline actions and differentiation (#3182)

This commit is contained in:
Mauro
2024-08-22 17:35:44 +02:00
committed by GitHub
parent d54e08e815
commit 4eb1ed5d09
44 changed files with 671 additions and 200 deletions
@@ -0,0 +1,161 @@
//
// Copyright 2024 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 Combine
import Foundation
enum PinnedEventsTimelineFlowCoordinatorAction {
case finished
case displayUser(userID: String)
case forwardedMessageToRoom(roomID: String)
case displayRoomScreenWithFocussedPin(eventID: String)
}
class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
private let navigationStackCoordinator: NavigationStackCoordinator
private let userSession: UserSessionProtocol
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
private let roomProxy: JoinedRoomProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let appMediator: AppMediatorProtocol
private let actionsSubject: PassthroughSubject<PinnedEventsTimelineFlowCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<PinnedEventsTimelineFlowCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
private var cancellables = Set<AnyCancellable>()
init(navigationStackCoordinator: NavigationStackCoordinator,
userSession: UserSessionProtocol,
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
roomProxy: JoinedRoomProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol,
appMediator: AppMediatorProtocol) {
self.navigationStackCoordinator = navigationStackCoordinator
self.userSession = userSession
self.roomTimelineControllerFactory = roomTimelineControllerFactory
self.roomProxy = roomProxy
self.userIndicatorController = userIndicatorController
self.appMediator = appMediator
}
func start() {
Task { await presentPinnedEventsTimeline() }
}
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
fatalError()
}
func clearRoute(animated: Bool) {
fatalError()
}
private func presentPinnedEventsTimeline() async {
let userID = userSession.clientProxy.userID
let timelineItemFactory = RoomTimelineItemFactory(userID: userID,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
guard let timelineController = await roomTimelineControllerFactory.buildRoomPinnedTimelineController(roomProxy: roomProxy, timelineItemFactory: timelineItemFactory) else {
fatalError("This can never fail because we allow this view to be presented only when the timeline is fully loaded and not nil")
}
let coordinator = PinnedEventsTimelineScreenCoordinator(parameters: .init(roomProxy: roomProxy,
timelineController: timelineController,
mediaProvider: userSession.mediaProvider,
mediaPlayerProvider: MediaPlayerProvider(),
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
appMediator: appMediator))
coordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .dismiss:
actionsSubject.send(.finished)
case .displayUser(let userID):
actionsSubject.send(.displayUser(userID: userID))
case .presentLocationViewer(let geoURI, let description):
presentMapNavigator(geoURI: geoURI, description: description)
case .displayMessageForwarding(let forwardingItem):
presentMessageForwarding(with: forwardingItem)
case .displayRoomScreenWithFocussedPin(let eventID):
actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID))
}
}
.store(in: &cancellables)
navigationStackCoordinator.setRootCoordinator(coordinator)
}
private func presentMapNavigator(geoURI: GeoURI, description: String?) {
let stackCoordinator = NavigationStackCoordinator()
let params = StaticLocationScreenCoordinatorParameters(interactionMode: .viewOnly(geoURI: geoURI, description: description), appMediator: appMediator)
let coordinator = StaticLocationScreenCoordinator(parameters: params)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .selectedLocation:
// We don't handle the sending/picker case in this flow
break
case .close:
self.navigationStackCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
stackCoordinator.setRootCoordinator(coordinator)
navigationStackCoordinator.setSheetCoordinator(stackCoordinator)
}
private func presentMessageForwarding(with forwardingItem: MessageForwardingItem) {
guard let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider else {
fatalError()
}
let stackCoordinator = NavigationStackCoordinator()
let parameters = MessageForwardingScreenCoordinatorParameters(forwardingItem: forwardingItem,
clientProxy: userSession.clientProxy,
roomSummaryProvider: roomSummaryProvider,
mediaProvider: userSession.mediaProvider,
userIndicatorController: userIndicatorController)
let coordinator = MessageForwardingScreenCoordinator(parameters: parameters)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil)
case .sent(let roomID):
navigationStackCoordinator.setSheetCoordinator(nil)
actionsSubject.send(.forwardedMessageToRoom(roomID: roomID))
}
}
.store(in: &cancellables)
stackCoordinator.setRootCoordinator(coordinator)
navigationStackCoordinator.setSheetCoordinator(stackCoordinator)
}
}
@@ -72,6 +72,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
// periphery:ignore - used to avoid deallocation
private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator?
// periphery:ignore - used to avoid deallocation
private var pinnedEventsTimelineFlowCoordinator: PinnedEventsTimelineFlowCoordinator?
// periphery:ignore - used to avoid deallocation
private var childRoomFlowCoordinator: RoomFlowCoordinator?
private let stateMachine: StateMachine<State, Event> = .init(state: .initial)
@@ -166,7 +168,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
} else if roomID != roomProxy.id {
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: via, entryPoint: .eventID(eventID)), userInfo: EventUserInfo(animated: animated))
} else {
roomScreenCoordinator?.focusOnEvent(eventID: eventID)
roomScreenCoordinator?.focusOnEvent(.init(eventID: eventID, shouldSetPin: false))
}
case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias:
break // These are converted to a room ID route one level above.
@@ -194,7 +196,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch room {
case .joined(let roomProxy):
await storeAndSubscribeToRoomProxy(roomProxy)
stateMachine.tryEvent(.presentRoom(focussedEventID: focussedEventID), userInfo: EventUserInfo(animated: animated))
let focussedEvent = focussedEventID.map { FocusEvent(eventID: $0, shouldSetPin: false) }
stateMachine.tryEvent(.presentRoom(focussedEvent: focussedEvent), userInfo: EventUserInfo(animated: animated))
default:
stateMachine.tryEvent(.presentJoinRoomScreen(via: via), userInfo: EventUserInfo(animated: animated))
}
@@ -270,16 +273,16 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
return .roomDetails(isRoot: false)
case (.room, .presentRoomMemberDetails(userID: let userID)):
return .roomMemberDetails(userID: userID, fromRoomMembersList: false)
return .roomMemberDetails(userID: userID, previousState: .room)
case (.roomMembersList, .presentRoomMemberDetails(userID: let userID)):
return .roomMemberDetails(userID: userID, fromRoomMembersList: true)
case (.roomMemberDetails(_, let fromRoomMembersList), .dismissRoomMemberDetails):
return fromRoomMembersList ? .roomMembersList : .room
return .roomMemberDetails(userID: userID, previousState: .roomMembersList)
case (.roomMemberDetails(_, let previousState), .dismissRoomMemberDetails):
return previousState
case (.roomMemberDetails(_, fromRoomMembersList: false), .presentUserProfile(let userID)):
return .userProfile(userID: userID)
case (.userProfile, .dismissUserProfile):
return .room
case (.roomMemberDetails(_, let previousState), .presentUserProfile(let userID)):
return .userProfile(userID: userID, previousState: previousState)
case (.userProfile(_, let previousState), .dismissUserProfile):
return previousState
case (.roomDetails, .presentInviteUsersScreen):
return .inviteUsersScreen(fromRoomMembersList: false)
@@ -351,6 +354,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
return .rolesAndPermissions
case (.rolesAndPermissions, .dismissRolesAndPermissionsScreen):
return .roomDetails(isRoot: false)
case (.roomDetails, .presentRoomMemberDetails(let userID)):
return .roomMemberDetails(userID: userID, previousState: fromState)
// Child flow
@@ -375,8 +381,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (_, .dismissJoinRoomScreen, .complete):
dismissFlow(animated: animated)
case (_, .presentRoom(let focussedEventID), .room):
Task { await self.presentRoom(fromState: context.fromState, focussedEventID: focussedEventID, animated: animated) }
case (_, .presentRoom(let focussedEvent), .room):
Task { await self.presentRoom(fromState: context.fromState, focussedEvent: focussedEvent, animated: animated) }
case (_, .dismissFlow, .complete):
dismissFlow(animated: animated)
@@ -470,7 +476,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
break
case (.room, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
Task { await self.presentPinnedEventsTimeline() }
presentPinnedEventsTimeline()
case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .room):
break
@@ -480,7 +486,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
break
case (.roomDetails, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
Task { await self.presentPinnedEventsTimeline() }
presentPinnedEventsTimeline()
case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .roomDetails):
break
@@ -493,6 +499,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
presentRolesAndPermissionsScreen()
case (.rolesAndPermissions, .dismissRolesAndPermissionsScreen, .roomDetails):
rolesAndPermissionsFlowCoordinator = nil
case (.roomDetails, .presentRoomMemberDetails(let userID), .roomMemberDetails):
presentRoomMemberDetails(userID: userID)
case (.roomMemberDetails, .dismissRoomMemberDetails, .roomDetails):
break
case (.roomMemberDetails, .dismissUserProfile, .roomDetails):
break
// Child flow
case (_, .startChildFlow(let roomID, let via, let entryPoint), .presentingChild):
@@ -525,9 +539,9 @@ 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.
/// - focussedEvent: An (optional) struct that contains the event ID that the timeline should be focussed around, and a boolean telling if such event should update the pinned events banner
/// - animated: whether it should animate the transition
private func presentRoom(fromState: State, focussedEventID: String? = nil, animated: Bool) async {
private func presentRoom(fromState: State, focussedEvent: FocusEvent?, 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)
@@ -545,8 +559,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
// 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)
if let focussedEvent {
roomScreenCoordinator?.focusOnEvent(focussedEvent)
}
return
@@ -565,7 +579,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(roomProxy: roomProxy,
initialFocussedEventID: focussedEventID,
initialFocussedEventID: focussedEvent?.eventID,
timelineItemFactory: timelineItemFactory)
self.timelineController = timelineController
@@ -576,7 +590,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
let composerDraftService = ComposerDraftService(roomProxy: roomProxy, timelineItemfactory: timelineItemFactory)
let parameters = RoomScreenCoordinatorParameters(roomProxy: roomProxy,
focussedEventID: focussedEventID,
focussedEvent: focussedEvent,
timelineController: timelineController,
mediaProvider: userSession.mediaProvider,
mediaPlayerProvider: MediaPlayerProvider(),
@@ -660,7 +674,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) {
await storeAndSubscribeToRoomProxy(roomProxy)
stateMachine.tryEvent(.presentRoom(focussedEventID: nil), userInfo: EventUserInfo(animated: animated))
stateMachine.tryEvent(.presentRoom(focussedEvent: nil), userInfo: EventUserInfo(animated: animated))
analytics.trackJoinedRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace, activeMemberCount: UInt(roomProxy.activeMembersCount))
} else {
@@ -964,42 +978,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func presentPinnedEventsTimeline() async {
let userID = userSession.clientProxy.userID
let timelineItemFactory = RoomTimelineItemFactory(userID: userID,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
guard let timelineController = await roomTimelineControllerFactory.buildRoomPinnedTimelineController(roomProxy: roomProxy, timelineItemFactory: timelineItemFactory) else {
fatalError("This can never fail because we allow this view to be presented only when the timeline is fully loaded and not nil")
}
let stackCoordinator = NavigationStackCoordinator()
let coordinator = PinnedEventsTimelineScreenCoordinator(parameters: .init(roomProxy: roomProxy,
timelineController: timelineController,
mediaProvider: userSession.mediaProvider,
mediaPlayerProvider: MediaPlayerProvider(),
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
appMediator: appMediator))
coordinator.actions
.sink { [weak self] action in
guard let self else { return }
switch action {
case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
stackCoordinator.setRootCoordinator(coordinator)
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
self?.stateMachine.tryEvent(.dismissPinnedEventsTimeline)
}
}
private func presentPollForm(mode: PollFormMode) {
let stackCoordinator = NavigationStackCoordinator()
let coordinator = PollFormScreenCoordinator(parameters: .init(mode: mode))
@@ -1345,6 +1323,43 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
coordinator.start()
}
private func presentPinnedEventsTimeline() {
let stackCoordinator = NavigationStackCoordinator()
let coordinator = PinnedEventsTimelineFlowCoordinator(navigationStackCoordinator: stackCoordinator,
userSession: userSession,
roomTimelineControllerFactory: roomTimelineControllerFactory,
roomProxy: roomProxy,
userIndicatorController: userIndicatorController,
appMediator: appMediator)
coordinator.actionsPublisher.sink { [weak self] action in
guard let self else {
return
}
switch action {
case .finished:
navigationStackCoordinator.setSheetCoordinator(nil)
case .displayUser(let userID):
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID))
case .forwardedMessageToRoom(let roomID):
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room))
case .displayRoomScreenWithFocussedPin(let eventID):
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.presentRoom(focussedEvent: .init(eventID: eventID, shouldSetPin: true)))
}
}
.store(in: &cancellables)
pinnedEventsTimelineFlowCoordinator = coordinator
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
self?.stateMachine.tryEvent(.dismissPinnedEventsTimeline)
}
coordinator.start()
}
// MARK: - Child Flow
private func startChildFlow(for roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) async {
@@ -1405,8 +1420,8 @@ private extension RoomFlowCoordinator {
case notificationSettings
case globalNotificationSettings
case roomMembersList
case roomMemberDetails(userID: String, fromRoomMembersList: Bool)
case userProfile(userID: String)
case roomMemberDetails(userID: String, previousState: State)
case userProfile(userID: String, previousState: State)
case inviteUsersScreen(fromRoomMembersList: Bool)
case mediaUploadPicker(source: MediaPickerScreenSource)
case mediaUploadPreview(fileURL: URL)
@@ -1434,7 +1449,7 @@ private extension RoomFlowCoordinator {
case presentJoinRoomScreen(via: [String])
case dismissJoinRoomScreen
case presentRoom(focussedEventID: String?)
case presentRoom(focussedEvent: FocusEvent?)
case dismissFlow
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
@@ -1497,12 +1512,6 @@ private extension RoomFlowCoordinator {
}
}
private extension GeoURI {
var bodyMessage: String {
"Location was shared at \(string)"
}
}
private extension Result {
var isFailure: Bool {
switch self {
@@ -1518,3 +1527,10 @@ private enum PinnedEventsTimelineSource: Hashable {
case room
case details(isRoot: Bool)
}
struct FocusEvent: Hashable {
/// The event ID that the timeline should be focussed around
let eventID: String
/// if the focus is coming from the pinned timeline, this should also update the pin banner
let shouldSetPin: Bool
}