mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Video playback (#308)
* Create media player screen * Introduce `FileCache` to cache message attachments * Add file loading functionality into the media provider * Process tap action on timeline items * Pass item taps to view model * Navigate to media player on view model callback * Commit project file * Add changelog * Rename media to video * Add a loader when large videos being processed * Add back button explicitly on video screen, fixes for light scheme * Handle right swipe to dismiss video
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct RoomScreenCoordinatorParameters {
|
||||
let navigationRouter: NavigationRouterType
|
||||
let timelineController: RoomTimelineControllerProtocol
|
||||
let mediaProvider: MediaProviderProtocol
|
||||
let roomName: String?
|
||||
@@ -31,6 +32,7 @@ final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
private let parameters: RoomScreenCoordinatorParameters
|
||||
private let roomScreenHostingController: UIViewController
|
||||
private var roomScreenViewModel: RoomScreenViewModelProtocol
|
||||
private var navigationRouter: NavigationRouterType { parameters.navigationRouter }
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@@ -55,7 +57,17 @@ final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() { }
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
roomScreenViewModel.callback = { [weak self] result in
|
||||
guard let self else { return }
|
||||
MXLog.debug("RoomScreenViewModel did complete with result: \(result).")
|
||||
switch result {
|
||||
case .displayVideo(let videoURL):
|
||||
self.displayVideo(for: videoURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
roomScreenHostingController
|
||||
@@ -64,4 +76,22 @@ final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
func stop() {
|
||||
roomScreenViewModel.stop()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func displayVideo(for videoURL: URL) {
|
||||
let params = VideoPlayerCoordinatorParameters(videoURL: videoURL)
|
||||
let coordinator = VideoPlayerCoordinator(parameters: params)
|
||||
coordinator.callback = { [weak self, weak coordinator] _ in
|
||||
guard let self, let coordinator = coordinator else { return }
|
||||
self.navigationRouter.popModule(animated: true)
|
||||
self.remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
add(childCoordinator: coordinator)
|
||||
coordinator.start()
|
||||
navigationRouter.push(coordinator) { [weak self] in
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum RoomScreenViewModelAction { }
|
||||
enum RoomScreenViewModelAction {
|
||||
case displayVideo(videoURL: URL)
|
||||
}
|
||||
|
||||
enum RoomScreenComposerMode: Equatable {
|
||||
case `default`
|
||||
@@ -29,6 +31,7 @@ enum RoomScreenViewAction {
|
||||
case loadPreviousPage
|
||||
case itemAppeared(id: String)
|
||||
case itemDisappeared(id: String)
|
||||
case itemTapped(id: String)
|
||||
case linkClicked(url: URL)
|
||||
case sendMessage
|
||||
case sendReaction(key: String, eventID: String)
|
||||
@@ -42,6 +45,7 @@ struct RoomScreenViewState: BindableState {
|
||||
var roomAvatar: UIImage?
|
||||
var items: [RoomTimelineViewProvider] = []
|
||||
var isBackPaginating = false
|
||||
var showLoading = false
|
||||
var bindings: RoomScreenViewStateBindings
|
||||
|
||||
var contextMenuBuilder: (@MainActor (_ itemId: String) -> TimelineItemContextMenu)?
|
||||
|
||||
@@ -81,14 +81,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
var callback: ((RoomScreenViewModelAction) -> Void)?
|
||||
|
||||
override func process(viewAction: RoomScreenViewAction) async {
|
||||
switch viewAction {
|
||||
case .loadPreviousPage:
|
||||
guard !state.isBackPaginating else {
|
||||
return
|
||||
}
|
||||
|
||||
switch await timelineController.paginateBackwards(Constants.backPaginationPageSize) {
|
||||
default:
|
||||
#warning("Treat errors")
|
||||
@@ -97,6 +95,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
await timelineController.processItemAppearance(id)
|
||||
case .itemDisappeared(let id):
|
||||
await timelineController.processItemDisappearance(id)
|
||||
case .itemTapped(let id):
|
||||
await itemTapped(with: id)
|
||||
case .linkClicked(let url):
|
||||
MXLog.warning("Link clicked: \(url)")
|
||||
case .sendMessage:
|
||||
@@ -118,6 +118,20 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func itemTapped(with itemId: String) async {
|
||||
state.showLoading = true
|
||||
let action = await timelineController.processItemTap(itemId)
|
||||
|
||||
switch action {
|
||||
case .displayVideo(let videoURL):
|
||||
callback?(.displayVideo(videoURL: videoURL))
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
||||
state.showLoading = false
|
||||
}
|
||||
|
||||
private func buildTimelineViews() {
|
||||
let stateItems = timelineController.timelineItems.map { item in
|
||||
|
||||
@@ -18,6 +18,7 @@ import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol RoomScreenViewModelProtocol {
|
||||
var callback: ((RoomScreenViewModelAction) -> Void)? { get set }
|
||||
var context: RoomScreenViewModelType.Context { get }
|
||||
|
||||
func stop()
|
||||
|
||||
@@ -20,30 +20,41 @@ struct RoomScreen: View {
|
||||
@ObservedObject var context: RoomScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0.0) {
|
||||
TimelineView()
|
||||
.environmentObject(context)
|
||||
|
||||
MessageComposer(text: $context.composerText,
|
||||
focused: $context.composerFocused,
|
||||
sendingDisabled: context.viewState.sendButtonDisabled,
|
||||
type: context.viewState.composerMode) {
|
||||
sendMessage()
|
||||
} replyCancellationAction: {
|
||||
context.send(viewAction: .cancelReply)
|
||||
} editCancellationAction: {
|
||||
context.send(viewAction: .cancelEdit)
|
||||
ZStack {
|
||||
VStack(spacing: 0.0) {
|
||||
TimelineView()
|
||||
.environmentObject(context)
|
||||
|
||||
MessageComposer(text: $context.composerText,
|
||||
focused: $context.composerFocused,
|
||||
sendingDisabled: context.viewState.sendButtonDisabled,
|
||||
type: context.viewState.composerMode) {
|
||||
sendMessage()
|
||||
} replyCancellationAction: {
|
||||
context.send(viewAction: .cancelReply)
|
||||
} editCancellationAction: {
|
||||
context.send(viewAction: .cancelEdit)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
RoomHeaderView(context: context)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
RoomHeaderView(context: context)
|
||||
}
|
||||
}
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
.sheet(item: $context.debugInfo) { DebugScreen(info: $0) }
|
||||
|
||||
if context.viewState.showLoading {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.tint(.element.primaryContent)
|
||||
.padding(16)
|
||||
.background(Color.element.quinaryContent)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
.sheet(item: $context.debugInfo) { DebugScreen(info: $0) }
|
||||
}
|
||||
|
||||
private func sendMessage() {
|
||||
|
||||
@@ -76,6 +76,9 @@ struct TimelineItemList: View {
|
||||
context.send(viewAction: .linkClicked(url: url))
|
||||
return .systemAction
|
||||
})
|
||||
.onTapGesture {
|
||||
context.send(viewAction: .itemTapped(id: item.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,6 +176,9 @@ struct TimelineItemList: View {
|
||||
}
|
||||
|
||||
private func requestBackPagination() {
|
||||
guard !context.viewState.isBackPaginating else {
|
||||
return
|
||||
}
|
||||
context.send(viewAction: .loadPreviousPage)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user