mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 20:09:04 +00:00
Media gallery - part 1(#3588)
* Introduce a `MediaEventsTimelineFlowCoordinator` * Update SDK API and architecture * Add a feature flag, add translations * Move the media events timeline presentation under the room flow coordinator state machine * Rename `TimelineViewState.timelineViewState` of type `TimelineState` to `timelineState` * Enabled SwiftLint's `trailing_closure` rule and fix the warnings.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
enum MediaEventsTimelineFlowCoordinatorAction {
|
||||
case finished
|
||||
}
|
||||
|
||||
class MediaEventsTimelineFlowCoordinator: 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 emojiProvider: EmojiProviderProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<MediaEventsTimelineFlowCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<MediaEventsTimelineFlowCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(navigationStackCoordinator: NavigationStackCoordinator,
|
||||
userSession: UserSessionProtocol,
|
||||
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
|
||||
roomProxy: JoinedRoomProxyProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol,
|
||||
appMediator: AppMediatorProtocol,
|
||||
emojiProvider: EmojiProviderProtocol) {
|
||||
self.navigationStackCoordinator = navigationStackCoordinator
|
||||
self.userSession = userSession
|
||||
self.roomTimelineControllerFactory = roomTimelineControllerFactory
|
||||
self.roomProxy = roomProxy
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.appMediator = appMediator
|
||||
self.emojiProvider = emojiProvider
|
||||
}
|
||||
|
||||
func start() {
|
||||
Task { await presentMediaEventsTimeline() }
|
||||
}
|
||||
|
||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func clearRoute(animated: Bool) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func presentMediaEventsTimeline() async {
|
||||
let timelineItemFactory = RoomTimelineItemFactory(userID: userSession.clientProxy.userID,
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userSession.clientProxy.userID))
|
||||
|
||||
guard case let .success(mediaTimelineController) = await roomTimelineControllerFactory.buildMessageFilteredRoomTimelineController(allowedMessageTypes: [.image, .video],
|
||||
roomProxy: roomProxy,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
mediaProvider: userSession.mediaProvider) else {
|
||||
MXLog.error("Failed presenting media timeline")
|
||||
return
|
||||
}
|
||||
|
||||
guard case let .success(filesTimelineController) = await roomTimelineControllerFactory.buildMessageFilteredRoomTimelineController(allowedMessageTypes: [.file, .audio],
|
||||
roomProxy: roomProxy,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
mediaProvider: userSession.mediaProvider) else {
|
||||
MXLog.error("Failed presenting media timeline")
|
||||
return
|
||||
}
|
||||
|
||||
let parameters = MediaEventsTimelineScreenCoordinatorParameters(roomProxy: roomProxy,
|
||||
mediaTimelineController: mediaTimelineController,
|
||||
filesTimelineController: filesTimelineController,
|
||||
mediaProvider: userSession.mediaProvider,
|
||||
mediaPlayerProvider: MediaPlayerProvider(),
|
||||
voiceMessageMediaManager: userSession.voiceMessageMediaManager,
|
||||
appMediator: appMediator,
|
||||
emojiProvider: emojiProvider)
|
||||
|
||||
let coordinator = MediaEventsTimelineScreenCoordinator(parameters: parameters)
|
||||
|
||||
navigationStackCoordinator.push(coordinator) { [weak self] in
|
||||
self?.actionsSubject.send(.finished)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,9 +65,9 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
||||
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
|
||||
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
|
||||
|
||||
guard let timelineController = await roomTimelineControllerFactory.buildRoomPinnedTimelineController(roomProxy: roomProxy,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
mediaProvider: userSession.mediaProvider) else {
|
||||
guard let timelineController = await roomTimelineControllerFactory.buildPinnedEventsRoomTimelineController(roomProxy: roomProxy,
|
||||
timelineItemFactory: timelineItemFactory,
|
||||
mediaProvider: userSession.mediaProvider) else {
|
||||
fatalError("This can never fail because we allow this view to be presented only when the timeline is fully loaded and not nil")
|
||||
}
|
||||
|
||||
|
||||
@@ -50,11 +50,6 @@ struct FocusEvent: Hashable {
|
||||
let shouldSetPin: Bool
|
||||
}
|
||||
|
||||
private enum PinnedEventsTimelineSource: Hashable {
|
||||
case room
|
||||
case details(isRoot: Bool)
|
||||
}
|
||||
|
||||
private enum PresentationAction: Hashable {
|
||||
case eventFocus(FocusEvent)
|
||||
case share(ShareExtensionPayload)
|
||||
@@ -102,6 +97,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
// periphery:ignore - used to avoid deallocation
|
||||
private var pinnedEventsTimelineFlowCoordinator: PinnedEventsTimelineFlowCoordinator?
|
||||
// periphery:ignore - used to avoid deallocation
|
||||
private var mediaEventsTimelineFlowCoordinator: MediaEventsTimelineFlowCoordinator?
|
||||
// periphery:ignore - used to avoid deallocation
|
||||
private var childRoomFlowCoordinator: RoomFlowCoordinator?
|
||||
|
||||
private let stateMachine: StateMachine<State, Event> = .init(state: .initial)
|
||||
@@ -149,7 +146,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
fatalError("This flow coordinator expect a route")
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||
guard stateMachine.state != .complete else {
|
||||
fatalError("This flow coordinator is `finished` ☠️")
|
||||
@@ -369,16 +365,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
return .room
|
||||
|
||||
case (.room, .presentPinnedEventsTimeline):
|
||||
return .pinnedEventsTimeline(previousState: .room)
|
||||
case (.roomDetails(let isRoot), .presentPinnedEventsTimeline):
|
||||
return .pinnedEventsTimeline(previousState: .details(isRoot: isRoot))
|
||||
return .pinnedEventsTimeline(previousState: fromState)
|
||||
case (.roomDetails, .presentPinnedEventsTimeline):
|
||||
return .pinnedEventsTimeline(previousState: fromState)
|
||||
case (.pinnedEventsTimeline(let previousState), .dismissPinnedEventsTimeline):
|
||||
switch previousState {
|
||||
case .room:
|
||||
return .room
|
||||
case .details(let isRoot):
|
||||
return .roomDetails(isRoot: isRoot)
|
||||
}
|
||||
return previousState
|
||||
|
||||
case (.roomDetails, .presentPollsHistory):
|
||||
return .pollsHistory
|
||||
@@ -400,8 +391,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case (.resolveSendFailure, .dismissResolveSendFailure):
|
||||
return .room
|
||||
|
||||
// Child flow
|
||||
|
||||
case (_, .startChildFlow(let roomID, _, _)):
|
||||
return .presentingChild(childRoomID: roomID, previousState: fromState)
|
||||
case (.presentingChild(_, let previousState), .dismissChildFlow):
|
||||
@@ -411,6 +400,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
return .knockRequestsList(previousState: fromState)
|
||||
case (.knockRequestsList(let previousState), .dismissKnockRequestsListScreen):
|
||||
return previousState
|
||||
|
||||
case (.roomDetails, .presentMediaEventsTimeline):
|
||||
return .mediaEventsTimeline(previousState: fromState)
|
||||
case (.mediaEventsTimeline(let previousState), .dismissMediaEventsTimeline):
|
||||
return previousState
|
||||
|
||||
default:
|
||||
return nil
|
||||
@@ -527,7 +521,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
break
|
||||
|
||||
case (.room, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
|
||||
presentPinnedEventsTimeline()
|
||||
startPinnedEventsTimelineFlow()
|
||||
case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .room):
|
||||
break
|
||||
|
||||
@@ -537,7 +531,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
break
|
||||
|
||||
case (.roomDetails, .presentPinnedEventsTimeline, .pinnedEventsTimeline):
|
||||
presentPinnedEventsTimeline()
|
||||
startPinnedEventsTimelineFlow()
|
||||
case (.pinnedEventsTimeline, .dismissPinnedEventsTimeline, .roomDetails):
|
||||
break
|
||||
|
||||
@@ -573,6 +567,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case (.knockRequestsList, .dismissKnockRequestsListScreen, .room):
|
||||
break
|
||||
|
||||
case (.roomDetails, .presentMediaEventsTimeline, .mediaEventsTimeline):
|
||||
Task { await self.startMediaEventsTimelineFlow() }
|
||||
case (.mediaEventsTimeline, .dismissMediaEventsTimeline, .roomDetails):
|
||||
break
|
||||
|
||||
// Child flow
|
||||
case (_, .startChildFlow(let roomID, let via, let entryPoint), .presentingChild):
|
||||
Task { await self.startChildFlow(for: roomID, via: via, entryPoint: entryPoint) }
|
||||
@@ -848,6 +847,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateMachine.tryEvent(.presentPinnedEventsTimeline)
|
||||
case .presentKnockingRequestsListScreen:
|
||||
stateMachine.tryEvent(.presentKnockRequestsListScreen)
|
||||
case .presentMediaEventsTimeline:
|
||||
stateMachine.tryEvent(.presentMediaEventsTimeline)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -1432,44 +1433,6 @@ 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,
|
||||
emojiProvider: emojiProvider)
|
||||
|
||||
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(presentationAction: .eventFocus(.init(eventID: eventID, shouldSetPin: true))))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
pinnedEventsTimelineFlowCoordinator = coordinator
|
||||
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
|
||||
self?.stateMachine.tryEvent(.dismissPinnedEventsTimeline)
|
||||
}
|
||||
coordinator.start()
|
||||
}
|
||||
|
||||
private func presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy) {
|
||||
let coordinator = ResolveVerifiedUserSendFailureScreenCoordinator(parameters: .init(failure: failure,
|
||||
sendHandle: sendHandle,
|
||||
@@ -1490,7 +1453,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Child Flow
|
||||
// MARK: - Other flows
|
||||
|
||||
private func startChildFlow(for roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) async {
|
||||
let coordinator = await RoomFlowCoordinator(roomID: roomID,
|
||||
@@ -1528,6 +1491,71 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
coordinator.handleAppRoute(.share(payload), animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func startPinnedEventsTimelineFlow() {
|
||||
let stackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let flowCoordinator = PinnedEventsTimelineFlowCoordinator(navigationStackCoordinator: stackCoordinator,
|
||||
userSession: userSession,
|
||||
roomTimelineControllerFactory: roomTimelineControllerFactory,
|
||||
roomProxy: roomProxy,
|
||||
userIndicatorController: userIndicatorController,
|
||||
appMediator: appMediator,
|
||||
emojiProvider: emojiProvider)
|
||||
|
||||
flowCoordinator.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(presentationAction: .eventFocus(.init(eventID: eventID, shouldSetPin: true))))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
pinnedEventsTimelineFlowCoordinator = flowCoordinator
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
|
||||
self?.stateMachine.tryEvent(.dismissPinnedEventsTimeline)
|
||||
}
|
||||
|
||||
flowCoordinator.start()
|
||||
}
|
||||
|
||||
private func startMediaEventsTimelineFlow() async {
|
||||
let flowCoordinator = MediaEventsTimelineFlowCoordinator(navigationStackCoordinator: navigationStackCoordinator,
|
||||
userSession: userSession,
|
||||
roomTimelineControllerFactory: roomTimelineControllerFactory,
|
||||
roomProxy: roomProxy,
|
||||
userIndicatorController: userIndicatorController,
|
||||
appMediator: appMediator,
|
||||
emojiProvider: emojiProvider)
|
||||
|
||||
flowCoordinator.actionsPublisher.sink { [weak self] action in
|
||||
guard let self else { return }
|
||||
|
||||
switch action {
|
||||
case .finished:
|
||||
stateMachine.tryEvent(.dismissMediaEventsTimeline)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
mediaEventsTimelineFlowCoordinator = flowCoordinator
|
||||
|
||||
flowCoordinator.start()
|
||||
}
|
||||
}
|
||||
|
||||
private extension RoomFlowCoordinator {
|
||||
@@ -1565,9 +1593,10 @@ private extension RoomFlowCoordinator {
|
||||
case pollsHistory
|
||||
case pollsHistoryForm
|
||||
case rolesAndPermissions
|
||||
case pinnedEventsTimeline(previousState: PinnedEventsTimelineSource)
|
||||
case pinnedEventsTimeline(previousState: State)
|
||||
case resolveSendFailure
|
||||
case knockRequestsList(previousState: State)
|
||||
case mediaEventsTimeline(previousState: State)
|
||||
|
||||
/// A child flow is in progress.
|
||||
case presentingChild(childRoomID: String, previousState: State)
|
||||
@@ -1643,12 +1672,14 @@ private extension RoomFlowCoordinator {
|
||||
case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy)
|
||||
case dismissResolveSendFailure
|
||||
|
||||
// Child room flow events
|
||||
case startChildFlow(roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint)
|
||||
case dismissChildFlow
|
||||
|
||||
case presentKnockRequestsListScreen
|
||||
case dismissKnockRequestsListScreen
|
||||
|
||||
case presentMediaEventsTimeline
|
||||
case dismissMediaEventsTimeline
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user