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:
Stefan Ceriu
2024-12-06 16:58:14 +02:00
committed by GitHub
parent a9e4837b62
commit caaa89af62
83 changed files with 1247 additions and 279 deletions
@@ -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
}
}