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:
ismailgulek
2022-11-14 11:58:18 +03:00
committed by GitHub
parent 8f1e8e6c2a
commit 2e56fe9380
27 changed files with 820 additions and 39 deletions
@@ -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)
}