mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:02:10 +00:00
File messages on timeline (#311)
* 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 * Add file messages into the timeline * Create file preview screen * Display files in the preview screen * Commit project file * Update Rust SDK to 1.0.19-alpha * Add changelog * Bump the RustSDK to `v1.0.20-alpha` * Configure audio session on video playback Co-authored-by: Stefan Ceriu <stefanc@matrix.org>
This commit is contained in:
@@ -116,11 +116,11 @@ class AppCoordinator: Coordinator {
|
||||
// This exposes the full Rust side tracing subscriber filter for more flexibility.
|
||||
// We can filter by level, crate and even file. See more details here:
|
||||
// https://docs.rs/tracing-subscriber/0.2.7/tracing_subscriber/filter/struct.EnvFilter.html#examples
|
||||
setupTracing(configuration: "warn,hyper=warn,sled=warn,matrix_sdk_sled=warn")
|
||||
setupTracing(filter: "warn,hyper=warn,sled=warn,matrix_sdk_sled=warn")
|
||||
|
||||
loggerConfiguration.logLevel = .debug
|
||||
#else
|
||||
setupTracing(configuration: "info,hyper=warn,sled=warn,matrix_sdk_sled=warn")
|
||||
setupTracing(filter: "info,hyper=warn,sled=warn,matrix_sdk_sled=warn")
|
||||
loggerConfiguration.logLevel = .info
|
||||
#endif
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// Copyright 2022 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 SwiftUI
|
||||
|
||||
struct FilePreviewCoordinatorParameters {
|
||||
let fileURL: URL
|
||||
let title: String?
|
||||
}
|
||||
|
||||
enum FilePreviewCoordinatorAction {
|
||||
case cancel
|
||||
}
|
||||
|
||||
final class FilePreviewCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: FilePreviewCoordinatorParameters
|
||||
private let filePreviewHostingController: UIViewController
|
||||
private var filePreviewViewModel: FilePreviewViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var activityIndicator: UserIndicator?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: ((FilePreviewCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: FilePreviewCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = FilePreviewViewModel(fileURL: parameters.fileURL, title: parameters.title)
|
||||
let view = FilePreviewScreen(context: viewModel.context)
|
||||
filePreviewViewModel = viewModel
|
||||
filePreviewHostingController = UIHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: filePreviewHostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
filePreviewViewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
MXLog.debug("FilePreviewViewModel did complete with result: \(action).")
|
||||
switch action {
|
||||
case .cancel:
|
||||
self.callback?(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
filePreviewHostingController
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
/// - label: The label to show on the indicator.
|
||||
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
|
||||
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
|
||||
activityIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
activityIndicator = nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Copyright 2022 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 Foundation
|
||||
|
||||
// MARK: - Coordinator
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum FilePreviewViewModelAction {
|
||||
case cancel
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
struct FilePreviewViewState: BindableState {
|
||||
let fileURL: URL
|
||||
let title: String?
|
||||
}
|
||||
|
||||
enum FilePreviewViewAction {
|
||||
case cancel
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// Copyright 2022 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 SwiftUI
|
||||
|
||||
typealias FilePreviewViewModelType = StateStoreViewModel<FilePreviewViewState, FilePreviewViewAction>
|
||||
|
||||
class FilePreviewViewModel: FilePreviewViewModelType, FilePreviewViewModelProtocol {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: ((FilePreviewViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(fileURL: URL, title: String? = nil) {
|
||||
super.init(initialViewState: FilePreviewViewState(fileURL: fileURL, title: title))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: FilePreviewViewAction) async {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
callback?(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright 2022 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 Foundation
|
||||
|
||||
@MainActor
|
||||
protocol FilePreviewViewModelProtocol {
|
||||
var callback: ((FilePreviewViewModelAction) -> Void)? { get set }
|
||||
var context: FilePreviewViewModelType.Context { get }
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// Copyright 2022 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 QuickLook
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct FilePreviewScreen: View {
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
var counterColor: Color {
|
||||
colorScheme == .light ? .element.secondaryContent : .element.tertiaryContent
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var context: FilePreviewViewModel.Context
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
PreviewController(fileURL: context.viewState.fileURL, title: context.viewState.title)
|
||||
}
|
||||
}
|
||||
|
||||
private struct PreviewController: UIViewControllerRepresentable {
|
||||
let fileURL: URL
|
||||
let title: String?
|
||||
|
||||
func makeUIViewController(context: Context) -> UINavigationController {
|
||||
let controller = QLPreviewController()
|
||||
controller.dataSource = context.coordinator
|
||||
|
||||
return UINavigationController(rootViewController: controller)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) { }
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(parent: self)
|
||||
}
|
||||
|
||||
class Coordinator: QLPreviewControllerDataSource {
|
||||
let parent: PreviewController
|
||||
|
||||
init(parent: PreviewController) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
||||
1
|
||||
}
|
||||
|
||||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
||||
PreviewItem(previewItemURL: parent.fileURL, previewItemTitle: parent.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PreviewItem: NSObject, QLPreviewItem {
|
||||
var previewItemURL: URL?
|
||||
var previewItemTitle: String?
|
||||
|
||||
init(previewItemURL: URL?, previewItemTitle: String?) {
|
||||
self.previewItemURL = previewItemURL
|
||||
self.previewItemTitle = previewItemTitle
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct FilePreview_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
let upgradeViewModel = FilePreviewViewModel(fileURL: URL(staticString: "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"))
|
||||
FilePreviewScreen(context: upgradeViewModel.context)
|
||||
}
|
||||
.tint(.element.accent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// Copyright 2022 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 SwiftUI
|
||||
|
||||
struct MediaPlayerCoordinatorParameters {
|
||||
let mediaURL: URL
|
||||
}
|
||||
|
||||
enum MediaPlayerCoordinatorAction {
|
||||
case cancel
|
||||
}
|
||||
|
||||
final class MediaPlayerCoordinator: Coordinator, Presentable {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: MediaPlayerCoordinatorParameters
|
||||
private let mediaPlayerHostingController: UIViewController
|
||||
private var mediaPlayerViewModel: MediaPlayerViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var activityIndicator: UserIndicator?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var callback: ((MediaPlayerCoordinatorAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: MediaPlayerCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
let viewModel = MediaPlayerViewModel(mediaURL: parameters.mediaURL)
|
||||
let view = MediaPlayerScreen(context: viewModel.context)
|
||||
mediaPlayerViewModel = viewModel
|
||||
mediaPlayerHostingController = UIHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: mediaPlayerHostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
mediaPlayerViewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
MXLog.debug("MediaPlayerViewModel did complete with result: \(action).")
|
||||
switch action {
|
||||
case .cancel:
|
||||
self.callback?(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
mediaPlayerHostingController
|
||||
}
|
||||
|
||||
func stop() {
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
/// - label: The label to show on the indicator.
|
||||
/// - isInteractionBlocking: Whether the indicator should block any user interaction.
|
||||
private func startLoading(label: String = ElementL10n.loading, isInteractionBlocking: Bool = true) {
|
||||
activityIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking))
|
||||
}
|
||||
|
||||
/// Hide the currently displayed activity indicator.
|
||||
private func stopLoading() {
|
||||
activityIndicator = nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Copyright 2022 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 Foundation
|
||||
|
||||
// MARK: - Coordinator
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum MediaPlayerViewModelAction {
|
||||
case cancel
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
struct MediaPlayerViewState: BindableState {
|
||||
let mediaURL: URL
|
||||
let autoplay: Bool
|
||||
}
|
||||
|
||||
enum MediaPlayerViewAction {
|
||||
case cancel
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// Copyright 2022 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 SwiftUI
|
||||
|
||||
typealias MediaPlayerViewModelType = StateStoreViewModel<MediaPlayerViewState, MediaPlayerViewAction>
|
||||
|
||||
class MediaPlayerViewModel: MediaPlayerViewModelType, MediaPlayerViewModelProtocol {
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var callback: ((MediaPlayerViewModelAction) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(mediaURL: URL, autoplay: Bool = true) {
|
||||
super.init(initialViewState: MediaPlayerViewState(mediaURL: mediaURL,
|
||||
autoplay: autoplay))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: MediaPlayerViewAction) async {
|
||||
switch viewAction {
|
||||
case .cancel:
|
||||
callback?(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright 2022 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 Foundation
|
||||
|
||||
@MainActor
|
||||
protocol MediaPlayerViewModelProtocol {
|
||||
var callback: ((MediaPlayerViewModelAction) -> Void)? { get set }
|
||||
var context: MediaPlayerViewModelType.Context { get }
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Copyright 2022 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 AVKit
|
||||
import SwiftUI
|
||||
|
||||
struct MediaPlayerScreen: View {
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var context: MediaPlayerViewModel.Context
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
VideoPlayer(player: player())
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
|
||||
private func player() -> AVPlayer {
|
||||
let player = AVPlayer(url: context.viewState.mediaURL)
|
||||
if context.viewState.autoplay {
|
||||
player.play()
|
||||
}
|
||||
return player
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct MediaPlayer_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
let viewModel = MediaPlayerViewModel(mediaURL: URL(staticString: "https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"),
|
||||
autoplay: false)
|
||||
MediaPlayerScreen(context: viewModel.context)
|
||||
}
|
||||
.tint(.element.accent)
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,8 @@ final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
switch result {
|
||||
case .displayVideo(let videoURL):
|
||||
self.displayVideo(for: videoURL)
|
||||
case .displayFile(let fileURL, let title):
|
||||
self.displayFile(for: fileURL, with: title)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,4 +96,20 @@ final class RoomScreenCoordinator: Coordinator, Presentable {
|
||||
self?.remove(childCoordinator: coordinator)
|
||||
}
|
||||
}
|
||||
|
||||
private func displayFile(for fileURL: URL, with title: String?) {
|
||||
let params = FilePreviewCoordinatorParameters(fileURL: fileURL, title: title)
|
||||
let coordinator = FilePreviewCoordinator(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import UIKit
|
||||
|
||||
enum RoomScreenViewModelAction {
|
||||
case displayVideo(videoURL: URL)
|
||||
case displayFile(fileURL: URL, title: String?)
|
||||
}
|
||||
|
||||
enum RoomScreenComposerMode: Equatable {
|
||||
|
||||
@@ -126,10 +126,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
switch action {
|
||||
case .displayVideo(let videoURL):
|
||||
callback?(.displayVideo(videoURL: videoURL))
|
||||
case .displayFile(let fileURL, let title):
|
||||
callback?(.displayFile(fileURL: fileURL, title: title))
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
||||
state.showLoading = false
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// Copyright 2022 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 Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct FileRoomTimelineView: View {
|
||||
let timelineItem: FileRoomTimelineItem
|
||||
|
||||
var body: some View {
|
||||
TimelineStyler(timelineItem: timelineItem) {
|
||||
HStack {
|
||||
Image(systemName: "doc.text.fill")
|
||||
.foregroundColor(.element.primaryContent)
|
||||
FormattedBodyText(text: timelineItem.text)
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 6)
|
||||
}
|
||||
.id(timelineItem.id)
|
||||
}
|
||||
}
|
||||
|
||||
struct FileRoomTimelineView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
body.preferredColorScheme(.light)
|
||||
body.preferredColorScheme(.dark)
|
||||
body.preferredColorScheme(.light)
|
||||
.timelineStyle(.plain)
|
||||
body.preferredColorScheme(.dark)
|
||||
.timelineStyle(.plain)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
static var body: some View {
|
||||
VStack(spacing: 20.0) {
|
||||
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "document.pdf",
|
||||
timestamp: "Now",
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
senderId: "Bob",
|
||||
source: nil,
|
||||
thumbnailSource: nil))
|
||||
|
||||
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "document.docx",
|
||||
timestamp: "Now",
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
senderId: "Bob",
|
||||
source: nil,
|
||||
thumbnailSource: nil))
|
||||
|
||||
FileRoomTimelineView(timelineItem: FileRoomTimelineItem(id: UUID().uuidString,
|
||||
text: "document.txt",
|
||||
timestamp: "Now",
|
||||
inGroupState: .single,
|
||||
isOutgoing: false,
|
||||
isEditable: false,
|
||||
senderId: "Bob",
|
||||
source: nil,
|
||||
thumbnailSource: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import AVKit
|
||||
import SwiftUI
|
||||
|
||||
struct VideoPlayerCoordinatorParameters {
|
||||
@@ -59,6 +60,9 @@ final class VideoPlayerCoordinator: Coordinator, Presentable {
|
||||
|
||||
func start() {
|
||||
MXLog.debug("Did start.")
|
||||
|
||||
configureAudioSession(.sharedInstance())
|
||||
|
||||
videoPlayerViewModel.callback = { [weak self] action in
|
||||
guard let self else { return }
|
||||
MXLog.debug("VideoPlayerViewModel did complete with result: \(action).")
|
||||
@@ -78,6 +82,17 @@ final class VideoPlayerCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func configureAudioSession(_ session: AVAudioSession) {
|
||||
do {
|
||||
try session.setCategory(.playback,
|
||||
mode: .default,
|
||||
options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP])
|
||||
try session.setActive(true)
|
||||
} catch {
|
||||
MXLog.debug("Configure audio session failed: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Show an activity indicator whilst loading.
|
||||
/// - Parameters:
|
||||
|
||||
@@ -75,7 +75,7 @@ class RoomProxy: RoomProxyProtocol {
|
||||
}
|
||||
|
||||
var isEncrypted: Bool {
|
||||
room.isEncrypted()
|
||||
(try? room.isEncrypted()) ?? false
|
||||
}
|
||||
|
||||
var isTombstoned: Bool {
|
||||
|
||||
@@ -124,6 +124,16 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
return .displayVideo(videoURL: videoURL)
|
||||
}
|
||||
return .none
|
||||
case let item as FileRoomTimelineItem:
|
||||
await loadFileForTimelineItem(item)
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == itemId }),
|
||||
let item = timelineItems[index] as? FileRoomTimelineItem else {
|
||||
return .none
|
||||
}
|
||||
if let fileURL = item.cachedFileURL {
|
||||
return .displayFile(fileURL: fileURL, title: item.text)
|
||||
}
|
||||
return .none
|
||||
default:
|
||||
return .none
|
||||
}
|
||||
@@ -338,6 +348,34 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func loadFileForTimelineItem(_ timelineItem: FileRoomTimelineItem) async {
|
||||
if timelineItem.cachedFileURL != nil {
|
||||
// already cached
|
||||
return
|
||||
}
|
||||
|
||||
guard let source = timelineItem.source else {
|
||||
return
|
||||
}
|
||||
|
||||
// This is not great. We could better estimate file extension from the mimetype.
|
||||
guard let fileExtension = timelineItem.text.split(separator: ".").last else {
|
||||
return
|
||||
}
|
||||
switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) {
|
||||
case .success(let fileURL):
|
||||
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
|
||||
var item = timelineItems[index] as? FileRoomTimelineItem else {
|
||||
return
|
||||
}
|
||||
|
||||
item.cachedFileURL = fileURL
|
||||
timelineItems[index] = item
|
||||
case .failure:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func loadUserAvatarForTimelineItem(_ timelineItem: EventBasedTimelineItemProtocol) async {
|
||||
if timelineItem.shouldShowSenderDetails == false {
|
||||
|
||||
@@ -26,6 +26,7 @@ enum RoomTimelineControllerCallback {
|
||||
|
||||
enum RoomTimelineControllerAction {
|
||||
case displayVideo(videoURL: URL)
|
||||
case displayFile(fileURL: URL, title: String?)
|
||||
case none
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ extension MessageTimelineItem where Content == MatrixRustSDK.ImageMessageContent
|
||||
|
||||
extension MatrixRustSDK.VideoMessageContent: MessageContentProtocol { }
|
||||
|
||||
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.image`.
|
||||
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.video`.
|
||||
extension MessageTimelineItem where Content == MatrixRustSDK.VideoMessageContent {
|
||||
var source: MediaSource {
|
||||
MediaSource(source: content.source)
|
||||
@@ -145,3 +145,19 @@ extension MessageTimelineItem where Content == MatrixRustSDK.VideoMessageContent
|
||||
content.info?.blurhash
|
||||
}
|
||||
}
|
||||
|
||||
extension MatrixRustSDK.FileMessageContent: MessageContentProtocol { }
|
||||
|
||||
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.file`.
|
||||
extension MessageTimelineItem where Content == MatrixRustSDK.FileMessageContent {
|
||||
var source: MediaSource {
|
||||
MediaSource(source: content.source)
|
||||
}
|
||||
|
||||
var thumbnailSource: MediaSource? {
|
||||
guard let src = content.info?.thumbnailSource else {
|
||||
return nil
|
||||
}
|
||||
return MediaSource(source: src)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// Copyright 2022 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 Foundation
|
||||
import UIKit
|
||||
|
||||
struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Equatable {
|
||||
let id: String
|
||||
let text: String
|
||||
let timestamp: String
|
||||
let inGroupState: TimelineItemInGroupState
|
||||
let isOutgoing: Bool
|
||||
let isEditable: Bool
|
||||
|
||||
let senderId: String
|
||||
var senderDisplayName: String?
|
||||
var senderAvatar: UIImage?
|
||||
|
||||
let source: MediaSource?
|
||||
let thumbnailSource: MediaSource?
|
||||
var cachedFileURL: URL?
|
||||
|
||||
var properties = RoomTimelineItemProperties()
|
||||
}
|
||||
@@ -62,6 +62,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
case .video(let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildVideoTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
|
||||
case .file(let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildFileTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
|
||||
case .notice(content: let content):
|
||||
let message = MessageTimelineItem(item: eventItemProxy.item, content: content)
|
||||
return buildNoticeTimelineItemFromMessage(message, isOutgoing, inGroupState, displayName, avatarImage)
|
||||
@@ -227,6 +230,26 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(message.reactions)))
|
||||
}
|
||||
|
||||
private func buildFileTimelineItemFromMessage(_ message: MessageTimelineItem<FileMessageContent>,
|
||||
_ isOutgoing: Bool,
|
||||
_ inGroupState: TimelineItemInGroupState,
|
||||
_ displayName: String?,
|
||||
_ avatarImage: UIImage?) -> RoomTimelineItemProtocol {
|
||||
FileRoomTimelineItem(id: message.id,
|
||||
text: message.body,
|
||||
timestamp: message.originServerTs.formatted(date: .omitted, time: .shortened),
|
||||
inGroupState: inGroupState,
|
||||
isOutgoing: isOutgoing,
|
||||
isEditable: message.isEditable,
|
||||
senderId: message.sender,
|
||||
senderDisplayName: displayName,
|
||||
senderAvatar: avatarImage,
|
||||
source: message.source,
|
||||
thumbnailSource: message.thumbnailSource,
|
||||
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
|
||||
reactions: aggregateReactions(message.reactions)))
|
||||
}
|
||||
|
||||
private func buildNoticeTimelineItemFromMessage(_ message: MessageTimelineItem<NoticeMessageContent>,
|
||||
_ isOutgoing: Bool,
|
||||
|
||||
@@ -25,6 +25,8 @@ struct RoomTimelineViewFactory: RoomTimelineViewFactoryProtocol {
|
||||
return .image(item)
|
||||
case let item as VideoRoomTimelineItem:
|
||||
return .video(item)
|
||||
case let item as FileRoomTimelineItem:
|
||||
return .file(item)
|
||||
case let item as SeparatorRoomTimelineItem:
|
||||
return .separator(item)
|
||||
case let item as NoticeRoomTimelineItem:
|
||||
|
||||
@@ -22,6 +22,7 @@ enum RoomTimelineViewProvider: Identifiable, Equatable {
|
||||
case separator(SeparatorRoomTimelineItem)
|
||||
case image(ImageRoomTimelineItem)
|
||||
case video(VideoRoomTimelineItem)
|
||||
case file(FileRoomTimelineItem)
|
||||
case emote(EmoteRoomTimelineItem)
|
||||
case notice(NoticeRoomTimelineItem)
|
||||
case redacted(RedactedRoomTimelineItem)
|
||||
@@ -37,6 +38,8 @@ enum RoomTimelineViewProvider: Identifiable, Equatable {
|
||||
return item.id
|
||||
case .video(let item):
|
||||
return item.id
|
||||
case .file(let item):
|
||||
return item.id
|
||||
case .emote(let item):
|
||||
return item.id
|
||||
case .notice(let item):
|
||||
@@ -60,6 +63,8 @@ extension RoomTimelineViewProvider: View {
|
||||
ImageRoomTimelineView(timelineItem: item)
|
||||
case .video(let item):
|
||||
VideoRoomTimelineView(timelineItem: item)
|
||||
case .file(let item):
|
||||
FileRoomTimelineView(timelineItem: item)
|
||||
case .emote(let item):
|
||||
EmoteRoomTimelineView(timelineItem: item)
|
||||
case .notice(let item):
|
||||
|
||||
Reference in New Issue
Block a user