mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
- room composer contextual menu for selecting source - coordinator for presenting the different sources - system picker handling and URL passback - file preview after selection - feature flag
This commit is contained in:
committed by
Stefan Ceriu
parent
2359910857
commit
5f6edacc1a
@@ -24,3 +24,11 @@
|
|||||||
"soft_logout_clear_data_submit" = "Clear all data";
|
"soft_logout_clear_data_submit" = "Clear all data";
|
||||||
"soft_logout_clear_data_dialog_title" = "Clear data";
|
"soft_logout_clear_data_dialog_title" = "Clear data";
|
||||||
"soft_logout_clear_data_dialog_content" = "Clear all data currently stored on this device?\nSign in again to access your account data and messages.";
|
"soft_logout_clear_data_dialog_content" = "Clear all data currently stored on this device?\nSign in again to access your account data and messages.";
|
||||||
|
|
||||||
|
// MARK: - Media upload
|
||||||
|
|
||||||
|
"media_upload_camera_picker" = "Camera";
|
||||||
|
|
||||||
|
"media_upload_photo_and_video_picker" = "Photo & Video Library";
|
||||||
|
|
||||||
|
"media_upload_document_picker" = "Document";
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ final class AppSettings: ObservableObject {
|
|||||||
case pusherProfileTag
|
case pusherProfileTag
|
||||||
case shouldCollapseRoomStateEvents
|
case shouldCollapseRoomStateEvents
|
||||||
case showStartChatFlow
|
case showStartChatFlow
|
||||||
|
case mediaUploadingFlowEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||||
@@ -162,4 +163,9 @@ final class AppSettings: ObservableObject {
|
|||||||
|
|
||||||
@UserSetting(key: UserDefaultsKeys.showStartChatFlow.rawValue, defaultValue: false, persistIn: store)
|
@UserSetting(key: UserDefaultsKeys.showStartChatFlow.rawValue, defaultValue: false, persistIn: store)
|
||||||
var startChatFlowFeatureFlag
|
var startChatFlowFeatureFlag
|
||||||
|
|
||||||
|
// MARK: Media Uploading
|
||||||
|
|
||||||
|
@UserSetting(key: UserDefaultsKeys.mediaUploadingFlowEnabled.rawValue, defaultValue: false, persistIn: nil)
|
||||||
|
var mediaUploadingFlowEnabled
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ public enum UntranslatedL10n {
|
|||||||
public static func analyticsOptInTitle(_ p1: Any) -> String {
|
public static func analyticsOptInTitle(_ p1: Any) -> String {
|
||||||
return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_title", String(describing: p1))
|
return UntranslatedL10n.tr("Untranslated", "analytics_opt_in_title", String(describing: p1))
|
||||||
}
|
}
|
||||||
|
/// Camera
|
||||||
|
public static var mediaUploadCameraPicker: String { return UntranslatedL10n.tr("Untranslated", "media_upload_camera_picker") }
|
||||||
|
/// Document
|
||||||
|
public static var mediaUploadDocumentPicker: String { return UntranslatedL10n.tr("Untranslated", "media_upload_document_picker") }
|
||||||
|
/// Photo & Video Library
|
||||||
|
public static var mediaUploadPhotoAndVideoPicker: String { return UntranslatedL10n.tr("Untranslated", "media_upload_photo_and_video_picker") }
|
||||||
/// Clear all data currently stored on this device?
|
/// Clear all data currently stored on this device?
|
||||||
/// Sign in again to access your account data and messages.
|
/// Sign in again to access your account data and messages.
|
||||||
public static var softLogoutClearDataDialogContent: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_dialog_content") }
|
public static var softLogoutClearDataDialogContent: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_dialog_content") }
|
||||||
|
|||||||
@@ -326,6 +326,27 @@ class RoomProxyMock: RoomProxyProtocol {
|
|||||||
return sendReactionToReturnValue
|
return sendReactionToReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//MARK: - sendImage
|
||||||
|
|
||||||
|
var sendImageBodyUrlCallsCount = 0
|
||||||
|
var sendImageBodyUrlCalled: Bool {
|
||||||
|
return sendImageBodyUrlCallsCount > 0
|
||||||
|
}
|
||||||
|
var sendImageBodyUrlReceivedArguments: (body: String, url: URL)?
|
||||||
|
var sendImageBodyUrlReceivedInvocations: [(body: String, url: URL)] = []
|
||||||
|
var sendImageBodyUrlReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var sendImageBodyUrlClosure: ((String, URL) async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func sendImage(body: String, url: URL) async -> Result<Void, RoomProxyError> {
|
||||||
|
sendImageBodyUrlCallsCount += 1
|
||||||
|
sendImageBodyUrlReceivedArguments = (body: body, url: url)
|
||||||
|
sendImageBodyUrlReceivedInvocations.append((body: body, url: url))
|
||||||
|
if let sendImageBodyUrlClosure = sendImageBodyUrlClosure {
|
||||||
|
return await sendImageBodyUrlClosure(body, url)
|
||||||
|
} else {
|
||||||
|
return sendImageBodyUrlReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
//MARK: - editMessage
|
//MARK: - editMessage
|
||||||
|
|
||||||
var editMessageOriginalCallsCount = 0
|
var editMessageOriginalCallsCount = 0
|
||||||
|
|||||||
@@ -31,4 +31,21 @@ extension FileManager {
|
|||||||
}
|
}
|
||||||
try createDirectory(at: url, withIntermediateDirectories: withIntermediateDirectories)
|
try createDirectory(at: url, withIntermediateDirectories: withIntermediateDirectories)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyFileToTemporaryLocation(url: URL) throws -> URL {
|
||||||
|
let newURL = URL.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
|
||||||
|
|
||||||
|
try? removeItem(at: newURL)
|
||||||
|
try copyItem(at: url, to: newURL)
|
||||||
|
|
||||||
|
return newURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeDataToTemporaryLocation(data: Data, fileName: String) throws -> URL {
|
||||||
|
let newURL = URL.temporaryDirectory.appendingPathComponent(fileName)
|
||||||
|
|
||||||
|
try data.write(to: newURL)
|
||||||
|
|
||||||
|
return newURL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ struct DeveloperOptionsScreenViewState: BindableState {
|
|||||||
struct DeveloperOptionsScreenViewStateBindings {
|
struct DeveloperOptionsScreenViewStateBindings {
|
||||||
var shouldCollapseRoomStateEvents: Bool
|
var shouldCollapseRoomStateEvents: Bool
|
||||||
var showStartChatFlow: Bool
|
var showStartChatFlow: Bool
|
||||||
|
var mediaUploadFlowEnabled: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DeveloperOptionsScreenViewAction {
|
enum DeveloperOptionsScreenViewAction {
|
||||||
case changedShouldCollapseRoomStateEvents
|
case changedShouldCollapseRoomStateEvents
|
||||||
case changedShowStartChatFlow
|
case changedShowStartChatFlow
|
||||||
|
case changedShowMediaUploadFlow
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -22,7 +22,9 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
|
|||||||
var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)?
|
var callback: ((DeveloperOptionsScreenViewModelAction) -> Void)?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: ServiceLocator.shared.settings.shouldCollapseRoomStateEvents, showStartChatFlow: ServiceLocator.shared.settings.startChatFlowFeatureFlag)
|
let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: ServiceLocator.shared.settings.shouldCollapseRoomStateEvents,
|
||||||
|
showStartChatFlow: ServiceLocator.shared.settings.startChatFlowFeatureFlag,
|
||||||
|
mediaUploadFlowEnabled: ServiceLocator.shared.settings.mediaUploadingFlowEnabled)
|
||||||
let state = DeveloperOptionsScreenViewState(bindings: bindings)
|
let state = DeveloperOptionsScreenViewState(bindings: bindings)
|
||||||
|
|
||||||
super.init(initialViewState: state)
|
super.init(initialViewState: state)
|
||||||
@@ -38,6 +40,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
|
|||||||
ServiceLocator.shared.settings.shouldCollapseRoomStateEvents = state.bindings.shouldCollapseRoomStateEvents
|
ServiceLocator.shared.settings.shouldCollapseRoomStateEvents = state.bindings.shouldCollapseRoomStateEvents
|
||||||
case .changedShowStartChatFlow:
|
case .changedShowStartChatFlow:
|
||||||
ServiceLocator.shared.settings.startChatFlowFeatureFlag = state.bindings.showStartChatFlow
|
ServiceLocator.shared.settings.startChatFlowFeatureFlag = state.bindings.showStartChatFlow
|
||||||
|
case .changedShowMediaUploadFlow:
|
||||||
|
ServiceLocator.shared.settings.mediaUploadingFlowEnabled = state.bindings.mediaUploadFlowEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -31,12 +31,20 @@ struct DeveloperOptionsScreenScreen: View {
|
|||||||
.onChange(of: context.shouldCollapseRoomStateEvents) { _ in
|
.onChange(of: context.shouldCollapseRoomStateEvents) { _ in
|
||||||
context.send(viewAction: .changedShouldCollapseRoomStateEvents)
|
context.send(viewAction: .changedShouldCollapseRoomStateEvents)
|
||||||
}
|
}
|
||||||
|
|
||||||
Toggle(isOn: $context.showStartChatFlow) {
|
Toggle(isOn: $context.showStartChatFlow) {
|
||||||
Text("Show Start Chat flow")
|
Text("Show Start Chat flow")
|
||||||
}
|
}
|
||||||
.onChange(of: context.showStartChatFlow) { _ in
|
.onChange(of: context.showStartChatFlow) { _ in
|
||||||
context.send(viewAction: .changedShowStartChatFlow)
|
context.send(viewAction: .changedShowStartChatFlow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Toggle(isOn: $context.mediaUploadFlowEnabled) {
|
||||||
|
Text("Show Media Uploading flow")
|
||||||
|
}
|
||||||
|
.onChange(of: context.mediaUploadFlowEnabled) { _ in
|
||||||
|
context.send(viewAction: .changedShowMediaUploadFlow)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2023 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
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
enum CameraPickerAction {
|
||||||
|
case selectFile(URL)
|
||||||
|
case cancel
|
||||||
|
case error(Error?)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CameraPicker: UIViewControllerRepresentable {
|
||||||
|
private let callback: (CameraPickerAction) -> Void
|
||||||
|
|
||||||
|
init(callback: @escaping (CameraPickerAction) -> Void) {
|
||||||
|
self.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||||
|
let imagePicker = UIImagePickerController()
|
||||||
|
imagePicker.sourceType = .camera
|
||||||
|
imagePicker.allowsEditing = true
|
||||||
|
imagePicker.delegate = context.coordinator
|
||||||
|
|
||||||
|
if let mediaTypes = UIImagePickerController.availableMediaTypes(for: .camera) {
|
||||||
|
imagePicker.mediaTypes = mediaTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
return imagePicker
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) { }
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||||
|
private var parent: CameraPicker
|
||||||
|
|
||||||
|
init(_ parent: CameraPicker) {
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||||
|
if let videoURL = info[.mediaURL] as? URL {
|
||||||
|
parent.callback(.selectFile(videoURL))
|
||||||
|
} else if let image = info[.originalImage] as? UIImage {
|
||||||
|
guard let jpegData = image.jpegData(compressionQuality: 1.0) else {
|
||||||
|
parent.callback(.error(nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileName = "\(UUID().uuidString).jpg"
|
||||||
|
|
||||||
|
do {
|
||||||
|
let url = try FileManager.default.writeDataToTemporaryLocation(data: jpegData, fileName: fileName)
|
||||||
|
parent.callback(.selectFile(url))
|
||||||
|
} catch {
|
||||||
|
parent.callback(.error(error))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parent.callback(.error(nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2023 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
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
enum DocumentPickerAction {
|
||||||
|
case selectFile(URL)
|
||||||
|
case cancel
|
||||||
|
case error(Error?)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DocumentPicker: UIViewControllerRepresentable {
|
||||||
|
private let callback: (DocumentPickerAction) -> Void
|
||||||
|
|
||||||
|
init(callback: @escaping (DocumentPickerAction) -> Void) {
|
||||||
|
self.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
|
||||||
|
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.data])
|
||||||
|
documentPicker.allowsMultipleSelection = false
|
||||||
|
documentPicker.delegate = context.coordinator
|
||||||
|
|
||||||
|
return documentPicker
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) { }
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Coordinator: NSObject, UIDocumentPickerDelegate {
|
||||||
|
private var parent: DocumentPicker
|
||||||
|
|
||||||
|
init(_ parent: DocumentPicker) {
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: UIDocumentPickerDelegate
|
||||||
|
|
||||||
|
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||||
|
parent.callback(.cancel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||||
|
guard let url = urls.first else {
|
||||||
|
parent.callback(.error(nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.callback(.selectFile(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2023 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
|
||||||
|
|
||||||
|
enum MediaPickerSource {
|
||||||
|
case camera
|
||||||
|
case photoLibrary
|
||||||
|
case documents
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MediaPickerCoordinatorAction {
|
||||||
|
case selectMediaAtURL(URL)
|
||||||
|
case cancel
|
||||||
|
case error(Error?)
|
||||||
|
}
|
||||||
|
|
||||||
|
class MediaPickerCoordinator: CoordinatorProtocol {
|
||||||
|
private let source: MediaPickerSource
|
||||||
|
private let callback: ((MediaPickerCoordinatorAction) -> Void)?
|
||||||
|
|
||||||
|
init(source: MediaPickerSource, callback: @escaping (MediaPickerCoordinatorAction) -> Void) {
|
||||||
|
self.source = source
|
||||||
|
self.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPresentable() -> AnyView {
|
||||||
|
AnyView(mediaPicker)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var mediaPicker: some View {
|
||||||
|
switch source {
|
||||||
|
case .camera:
|
||||||
|
cameraPicker
|
||||||
|
case .photoLibrary:
|
||||||
|
PhotoLibraryPicker { [weak self] action in
|
||||||
|
switch action {
|
||||||
|
case .cancel:
|
||||||
|
self?.callback?(.cancel)
|
||||||
|
case .error(let error):
|
||||||
|
self?.callback?(.error(error))
|
||||||
|
case .selectFile(let url):
|
||||||
|
self?.callback?(.selectMediaAtURL(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .documents:
|
||||||
|
// The document picker automatically dismisses everything on selection
|
||||||
|
// Strongly retain self in the callback to forward actions correctly
|
||||||
|
DocumentPicker { action in
|
||||||
|
switch action {
|
||||||
|
case .cancel:
|
||||||
|
self.callback?(.cancel)
|
||||||
|
case .error(let error):
|
||||||
|
self.callback?(.error(error))
|
||||||
|
case .selectFile(let url):
|
||||||
|
self.callback?(.selectMediaAtURL(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cameraPicker: some View {
|
||||||
|
CameraPicker { [weak self] action in
|
||||||
|
switch action {
|
||||||
|
case .cancel:
|
||||||
|
self?.callback?(.cancel)
|
||||||
|
case .error(let error):
|
||||||
|
self?.callback?(.error(error))
|
||||||
|
case .selectFile(let url):
|
||||||
|
self?.callback?(.selectMediaAtURL(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(.black, ignoresSafeAreaEdges: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2023 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 PhotosUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum PhotoLibraryPickerAction {
|
||||||
|
case selectFile(URL)
|
||||||
|
case cancel
|
||||||
|
case error(Error?)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PhotoLibraryPicker: UIViewControllerRepresentable {
|
||||||
|
private let callback: (PhotoLibraryPickerAction) -> Void
|
||||||
|
|
||||||
|
init(callback: @escaping (PhotoLibraryPickerAction) -> Void) {
|
||||||
|
self.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||||
|
var configuration = PHPickerConfiguration(photoLibrary: .shared())
|
||||||
|
configuration.selectionLimit = 1
|
||||||
|
|
||||||
|
let pickerViewController = PHPickerViewController(configuration: configuration)
|
||||||
|
pickerViewController.delegate = context.coordinator
|
||||||
|
|
||||||
|
return pickerViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) { }
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||||||
|
private var parent: PhotoLibraryPicker
|
||||||
|
|
||||||
|
init(_ parent: PhotoLibraryPicker) {
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: PHPickerViewControllerDelegate
|
||||||
|
|
||||||
|
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||||
|
guard let provider = results.first?.itemProvider else {
|
||||||
|
parent.callback(.cancel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
provider.loadFileRepresentation(forTypeIdentifier: "public.item") { @MainActor [weak self] url, error in
|
||||||
|
guard let url else {
|
||||||
|
self?.parent.callback(.error(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let _ = url.startAccessingSecurityScopedResource()
|
||||||
|
let newURL = try FileManager.default.copyFileToTemporaryLocation(url: url)
|
||||||
|
url.stopAccessingSecurityScopedResource()
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
|
self?.parent.callback(.selectFile(newURL))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
self?.parent.callback(.error(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
//
|
||||||
|
// 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 MediaPickerPreviewScreenCoordinatorParameters {
|
||||||
|
let url: URL
|
||||||
|
let title: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MediaPickerPreviewScreenCoordinatorAction {
|
||||||
|
case send
|
||||||
|
case cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MediaPickerPreviewScreenCoordinator: CoordinatorProtocol {
|
||||||
|
private let parameters: MediaPickerPreviewScreenCoordinatorParameters
|
||||||
|
|
||||||
|
private var viewModel: MediaPickerPreviewScreenViewModelProtocol
|
||||||
|
private let callback: (MediaPickerPreviewScreenCoordinatorAction) -> Void
|
||||||
|
|
||||||
|
init(parameters: MediaPickerPreviewScreenCoordinatorParameters, callback: @escaping (MediaPickerPreviewScreenCoordinatorAction) -> Void) {
|
||||||
|
self.parameters = parameters
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
viewModel = MediaPickerPreviewScreenViewModel(url: parameters.url, title: parameters.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
viewModel.callback = { [weak self] action in
|
||||||
|
switch action {
|
||||||
|
case .send:
|
||||||
|
self?.callback(.send)
|
||||||
|
case .cancel:
|
||||||
|
self?.callback(.cancel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPresentable() -> AnyView {
|
||||||
|
AnyView(MediaPickerPreviewScreenScreen(context: viewModel.context))
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-13
@@ -14,18 +14,19 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import Foundation
|
||||||
|
|
||||||
class ActivityCoordinator: CoordinatorProtocol {
|
enum MediaPickerPreviewScreenViewModelAction {
|
||||||
let items: [Any]
|
case send
|
||||||
|
case cancel
|
||||||
init(items: [Any]) {
|
}
|
||||||
self.items = items
|
|
||||||
}
|
struct MediaPickerPreviewScreenViewState: BindableState {
|
||||||
|
let url: URL
|
||||||
func toPresentable() -> AnyView {
|
let title: String?
|
||||||
AnyView(UIActivityViewControllerWrapper(activityItems: items)
|
}
|
||||||
.presentationDetents([.medium])
|
|
||||||
.ignoresSafeArea())
|
enum MediaPickerPreviewScreenViewAction {
|
||||||
}
|
case send
|
||||||
|
case cancel
|
||||||
}
|
}
|
||||||
+36
@@ -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 SwiftUI
|
||||||
|
|
||||||
|
typealias MediaPickerPreviewScreenViewModelType = StateStoreViewModel<MediaPickerPreviewScreenViewState, MediaPickerPreviewScreenViewAction>
|
||||||
|
|
||||||
|
class MediaPickerPreviewScreenViewModel: MediaPickerPreviewScreenViewModelType, MediaPickerPreviewScreenViewModelProtocol {
|
||||||
|
var callback: ((MediaPickerPreviewScreenViewModelAction) -> Void)?
|
||||||
|
|
||||||
|
init(url: URL, title: String?) {
|
||||||
|
super.init(initialViewState: MediaPickerPreviewScreenViewState(url: url, title: title))
|
||||||
|
}
|
||||||
|
|
||||||
|
override func process(viewAction: MediaPickerPreviewScreenViewAction) async {
|
||||||
|
switch viewAction {
|
||||||
|
case .send:
|
||||||
|
callback?(.send)
|
||||||
|
case .cancel:
|
||||||
|
callback?(.cancel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -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 MediaPickerPreviewScreenViewModelProtocol {
|
||||||
|
var callback: ((MediaPickerPreviewScreenViewModelAction) -> Void)? { get set }
|
||||||
|
var context: MediaPickerPreviewScreenViewModelType.Context { get }
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
struct MediaPickerPreviewScreenScreen: View {
|
||||||
|
@ObservedObject var context: MediaPickerPreviewScreenViewModel.Context
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
PreviewView(context: context,
|
||||||
|
fileURL: context.viewState.url,
|
||||||
|
title: context.viewState.title)
|
||||||
|
.id(UUID())
|
||||||
|
.ignoresSafeArea(edges: .bottom)
|
||||||
|
.toolbar { toolbar }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ToolbarContentBuilder
|
||||||
|
private var toolbar: some ToolbarContent {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
Button { context.send(viewAction: .cancel) } label: {
|
||||||
|
Text(L10n.actionCancel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .confirmationAction) {
|
||||||
|
Button { context.send(viewAction: .send) } label: {
|
||||||
|
Text(L10n.actionSend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PreviewView: UIViewControllerRepresentable {
|
||||||
|
let context: MediaPickerPreviewScreenViewModel.Context
|
||||||
|
let fileURL: URL
|
||||||
|
let title: String?
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UINavigationController {
|
||||||
|
let previewController = QLPreviewController()
|
||||||
|
previewController.dataSource = context.coordinator
|
||||||
|
previewController.delegate = context.coordinator
|
||||||
|
|
||||||
|
return UINavigationController(rootViewController: previewController)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) { }
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(view: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate {
|
||||||
|
let view: PreviewView
|
||||||
|
|
||||||
|
init(view: PreviewView) {
|
||||||
|
self.view = view
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - QLPreviewControllerDataSource
|
||||||
|
|
||||||
|
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
||||||
|
PreviewItem(previewItemURL: view.fileURL, previewItemTitle: view.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - QLPreviewControllerDelegate
|
||||||
|
|
||||||
|
func previewController(_ controller: QLPreviewController, editingModeFor previewItem: QLPreviewItem) -> QLPreviewItemEditingMode {
|
||||||
|
.disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 MediaPickerPreviewScreen_Previews: PreviewProvider {
|
||||||
|
static let viewModel = MediaPickerPreviewScreenViewModel(url: URL.picturesDirectory, title: nil)
|
||||||
|
static var previews: some View {
|
||||||
|
MediaPickerPreviewScreenScreen(context: viewModel.context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,6 +62,12 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
self.displayEmojiPickerScreen(for: itemId)
|
self.displayEmojiPickerScreen(for: itemId)
|
||||||
case .displayReportContent(let itemId):
|
case .displayReportContent(let itemId):
|
||||||
self.displayReportContent(for: itemId)
|
self.displayReportContent(for: itemId)
|
||||||
|
case .displayCameraPicker:
|
||||||
|
self.displayMediaPickerWithSource(.camera)
|
||||||
|
case .displayMediaPicker:
|
||||||
|
self.displayMediaPickerWithSource(.photoLibrary)
|
||||||
|
case .displayDocumentPicker:
|
||||||
|
self.displayMediaPickerWithSource(.documents)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +83,30 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func displayMediaPickerWithSource(_ source: MediaPickerSource) {
|
||||||
|
let mediaPickerCoordinator = MediaPickerCoordinator(source: source) { [weak self] action in
|
||||||
|
switch action {
|
||||||
|
case .cancel:
|
||||||
|
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
case .error:
|
||||||
|
break
|
||||||
|
case .selectMediaAtURL(let url):
|
||||||
|
let mediaPickerPreviewScreenCoordinator = MediaPickerPreviewScreenCoordinator(parameters: .init(url: url, title: url.lastPathComponent)) { action in
|
||||||
|
switch action {
|
||||||
|
case .send:
|
||||||
|
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
case .cancel:
|
||||||
|
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self?.navigationStackCoordinator.setSheetCoordinator(mediaPickerPreviewScreenCoordinator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(mediaPickerCoordinator)
|
||||||
|
}
|
||||||
|
|
||||||
private func displayFilePreview(for file: MediaFileHandleProxy, with title: String?) {
|
private func displayFilePreview(for file: MediaFileHandleProxy, with title: String?) {
|
||||||
let params = FilePreviewCoordinatorParameters(mediaFile: file, title: title)
|
let params = FilePreviewCoordinatorParameters(mediaFile: file, title: title)
|
||||||
let coordinator = FilePreviewCoordinator(parameters: params)
|
let coordinator = FilePreviewCoordinator(parameters: params)
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ enum RoomScreenViewModelAction {
|
|||||||
case displayMediaFile(file: MediaFileHandleProxy, title: String?)
|
case displayMediaFile(file: MediaFileHandleProxy, title: String?)
|
||||||
case displayEmojiPicker(itemId: String)
|
case displayEmojiPicker(itemId: String)
|
||||||
case displayReportContent(itemId: String)
|
case displayReportContent(itemId: String)
|
||||||
|
case displayCameraPicker
|
||||||
|
case displayMediaPicker
|
||||||
|
case displayDocumentPicker
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomScreenComposerMode: Equatable {
|
enum RoomScreenComposerMode: Equatable {
|
||||||
@@ -55,6 +58,10 @@ enum RoomScreenViewAction {
|
|||||||
/// Mark the entire room as read - this is heavy handed as a starting point for now.
|
/// Mark the entire room as read - this is heavy handed as a starting point for now.
|
||||||
case markRoomAsRead
|
case markRoomAsRead
|
||||||
case contextMenuAction(itemID: String, action: TimelineItemContextMenuAction)
|
case contextMenuAction(itemID: String, action: TimelineItemContextMenuAction)
|
||||||
|
|
||||||
|
case displayCameraPicker
|
||||||
|
case displayMediaPicker
|
||||||
|
case displayDocumentPicker
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomScreenViewState: BindableState {
|
struct RoomScreenViewState: BindableState {
|
||||||
@@ -66,6 +73,7 @@ struct RoomScreenViewState: BindableState {
|
|||||||
var isBackPaginating = false
|
var isBackPaginating = false
|
||||||
var showLoading = false
|
var showLoading = false
|
||||||
var timelineStyle: TimelineStyle
|
var timelineStyle: TimelineStyle
|
||||||
|
var mediaUploadingFlowEnabled: Bool
|
||||||
|
|
||||||
var bindings: RoomScreenViewStateBindings
|
var bindings: RoomScreenViewStateBindings
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
roomTitle: roomName ?? "Unknown room 💥",
|
roomTitle: roomName ?? "Unknown room 💥",
|
||||||
roomAvatarURL: roomAvatarUrl,
|
roomAvatarURL: roomAvatarUrl,
|
||||||
timelineStyle: ServiceLocator.shared.settings.timelineStyle,
|
timelineStyle: ServiceLocator.shared.settings.timelineStyle,
|
||||||
|
mediaUploadingFlowEnabled: ServiceLocator.shared.settings.mediaUploadingFlowEnabled,
|
||||||
bindings: .init(composerText: "", composerFocused: false)),
|
bindings: .init(composerText: "", composerFocused: false)),
|
||||||
imageProvider: mediaProvider)
|
imageProvider: mediaProvider)
|
||||||
|
|
||||||
@@ -74,6 +75,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
.weakAssign(to: \.state.timelineStyle, on: self)
|
.weakAssign(to: \.state.timelineStyle, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
ServiceLocator.shared.settings.$mediaUploadingFlowEnabled
|
||||||
|
.weakAssign(to: \.state.mediaUploadingFlowEnabled, on: self)
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
buildTimelineViews()
|
buildTimelineViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +116,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
await markRoomAsRead()
|
await markRoomAsRead()
|
||||||
case .contextMenuAction(let itemID, let action):
|
case .contextMenuAction(let itemID, let action):
|
||||||
processContentMenuAction(action, itemID: itemID)
|
processContentMenuAction(action, itemID: itemID)
|
||||||
|
case .displayCameraPicker:
|
||||||
|
callback?(.displayCameraPicker)
|
||||||
|
case .displayMediaPicker:
|
||||||
|
callback?(.displayMediaPicker)
|
||||||
|
case .displayDocumentPicker:
|
||||||
|
callback?(.displayDocumentPicker)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,16 @@ struct RoomScreen: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
timeline
|
timeline
|
||||||
.background(Color.element.background.ignoresSafeArea()) // Kills the toolbar translucency.
|
.background(Color.element.background.ignoresSafeArea()) // Kills the toolbar translucency.
|
||||||
.safeAreaInset(edge: .bottom, spacing: 0) { messageComposer }
|
.safeAreaInset(edge: .bottom, spacing: 0) {
|
||||||
|
HStack(spacing: 4.0) {
|
||||||
|
if context.viewState.mediaUploadingFlowEnabled {
|
||||||
|
sendAttachmentButton
|
||||||
|
}
|
||||||
|
|
||||||
|
messageComposer
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar { toolbar }
|
.toolbar { toolbar }
|
||||||
.toolbarRole(.editor) // Hide the back button title.
|
.toolbarRole(.editor) // Hide the back button title.
|
||||||
@@ -40,7 +49,7 @@ struct RoomScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var timeline: some View {
|
private var timeline: some View {
|
||||||
TimelineView()
|
TimelineView()
|
||||||
.id(context.viewState.roomId)
|
.id(context.viewState.roomId)
|
||||||
.environmentObject(context)
|
.environmentObject(context)
|
||||||
@@ -48,7 +57,7 @@ struct RoomScreen: View {
|
|||||||
.overlay(alignment: .bottomTrailing) { scrollToBottomButton }
|
.overlay(alignment: .bottomTrailing) { scrollToBottomButton }
|
||||||
}
|
}
|
||||||
|
|
||||||
var messageComposer: some View {
|
private var messageComposer: some View {
|
||||||
MessageComposer(text: $context.composerText,
|
MessageComposer(text: $context.composerText,
|
||||||
focused: $context.composerFocused,
|
focused: $context.composerFocused,
|
||||||
sendingDisabled: context.viewState.sendButtonDisabled,
|
sendingDisabled: context.viewState.sendButtonDisabled,
|
||||||
@@ -59,10 +68,9 @@ struct RoomScreen: View {
|
|||||||
} editCancellationAction: {
|
} editCancellationAction: {
|
||||||
context.send(viewAction: .cancelEdit)
|
context.send(viewAction: .cancelEdit)
|
||||||
}
|
}
|
||||||
.padding()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var scrollToBottomButton: some View {
|
private var scrollToBottomButton: some View {
|
||||||
Button { context.viewState.scrollToBottomPublisher.send(()) } label: {
|
Button { context.viewState.scrollToBottomPublisher.send(()) } label: {
|
||||||
Image(systemName: "chevron.down")
|
Image(systemName: "chevron.down")
|
||||||
.font(.element.body)
|
.font(.element.body)
|
||||||
@@ -84,7 +92,7 @@ struct RoomScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var loadingIndicator: some View {
|
private var loadingIndicator: some View {
|
||||||
if context.viewState.showLoading {
|
if context.viewState.showLoading {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.progressViewStyle(.circular)
|
.progressViewStyle(.circular)
|
||||||
@@ -96,7 +104,7 @@ struct RoomScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ToolbarContentBuilder
|
@ToolbarContentBuilder
|
||||||
var toolbar: some ToolbarContent {
|
private var toolbar: some ToolbarContent {
|
||||||
// .principal + .primaryAction works better than .navigation leading + trailing
|
// .principal + .primaryAction works better than .navigation leading + trailing
|
||||||
// as the latter disables interaction in the action button for rooms with long names
|
// as the latter disables interaction in the action button for rooms with long names
|
||||||
ToolbarItem(placement: .principal) {
|
ToolbarItem(placement: .principal) {
|
||||||
@@ -104,6 +112,30 @@ struct RoomScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var sendAttachmentButton: some View {
|
||||||
|
Menu {
|
||||||
|
Button {
|
||||||
|
context.send(viewAction: .displayDocumentPicker)
|
||||||
|
} label: {
|
||||||
|
Label(UntranslatedL10n.mediaUploadDocumentPicker, systemImage: "doc")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
context.send(viewAction: .displayMediaPicker)
|
||||||
|
} label: {
|
||||||
|
Label(UntranslatedL10n.mediaUploadPhotoAndVideoPicker, systemImage: "photo")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
context.send(viewAction: .displayCameraPicker)
|
||||||
|
} label: {
|
||||||
|
Label(UntranslatedL10n.mediaUploadCameraPicker, systemImage: "camera")
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "plus.circle")
|
||||||
|
.font(.element.title1)
|
||||||
|
.foregroundColor(.element.brand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func sendMessage() {
|
private func sendMessage() {
|
||||||
guard !context.viewState.sendButtonDisabled else { return }
|
guard !context.viewState.sendButtonDisabled else { return }
|
||||||
context.send(viewAction: .sendMessage)
|
context.send(viewAction: .sendMessage)
|
||||||
|
|||||||
@@ -189,15 +189,15 @@ class RoomProxy: RoomProxyProtocol {
|
|||||||
let transactionId = genTransactionId()
|
let transactionId = genTransactionId()
|
||||||
|
|
||||||
return await Task.dispatch(on: userInitiatedDispatchQueue) {
|
return await Task.dispatch(on: userInitiatedDispatchQueue) {
|
||||||
if let eventID {
|
do {
|
||||||
do {
|
if let eventID {
|
||||||
try self.room.sendReply(msg: message, inReplyToEventId: eventID, txnId: transactionId)
|
try self.room.sendReply(msg: message, inReplyToEventId: eventID, txnId: transactionId)
|
||||||
} catch {
|
} else {
|
||||||
return .failure(.failedSendingMessage)
|
let messageContent = messageEventContentFromMarkdown(md: message)
|
||||||
|
try self.room.send(msg: messageContent, txnId: transactionId)
|
||||||
}
|
}
|
||||||
} else {
|
} catch {
|
||||||
let messageContent = messageEventContentFromMarkdown(md: message)
|
return .failure(.failedSendingMessage)
|
||||||
self.room.send(msg: messageContent, txnId: transactionId)
|
|
||||||
}
|
}
|
||||||
return .success(())
|
return .success(())
|
||||||
}
|
}
|
||||||
@@ -219,6 +219,10 @@ class RoomProxy: RoomProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendImage(body: String, url: URL) async -> Result<Void, RoomProxyError> {
|
||||||
|
.failure(.failedSendingMedia)
|
||||||
|
}
|
||||||
|
|
||||||
func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError> {
|
func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError> {
|
||||||
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||||
defer {
|
defer {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ enum RoomProxyError: Error {
|
|||||||
case failedSendingReadReceipt
|
case failedSendingReadReceipt
|
||||||
case failedSendingMessage
|
case failedSendingMessage
|
||||||
case failedSendingReaction
|
case failedSendingReaction
|
||||||
|
case failedSendingMedia
|
||||||
case failedEditingMessage
|
case failedEditingMessage
|
||||||
case failedRedactingEvent
|
case failedRedactingEvent
|
||||||
case failedReportingContent
|
case failedReportingContent
|
||||||
@@ -70,6 +71,8 @@ protocol RoomProxyProtocol {
|
|||||||
|
|
||||||
func sendReaction(_ reaction: String, to eventID: String) async -> Result<Void, RoomProxyError>
|
func sendReaction(_ reaction: String, to eventID: String) async -> Result<Void, RoomProxyError>
|
||||||
|
|
||||||
|
func sendImage(body: String, url: URL) async -> Result<Void, RoomProxyError>
|
||||||
|
|
||||||
func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError>
|
func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError>
|
||||||
|
|
||||||
func redact(_ eventID: String) async -> Result<Void, RoomProxyError>
|
func redact(_ eventID: String) async -> Result<Void, RoomProxyError>
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>The camera is used to take and upload photos and videos.</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>The microphone is used to take videos.</string>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>NSUserActivityTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>INSendMessageIntent</string>
|
<string>INSendMessageIntent</string>
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ targets:
|
|||||||
NSUserActivityTypes: [
|
NSUserActivityTypes: [
|
||||||
INSendMessageIntent
|
INSendMessageIntent
|
||||||
]
|
]
|
||||||
|
NSCameraUsageDescription: The camera is used to take and upload photos and videos.
|
||||||
|
NSMicrophoneUsageDescription: The microphone is used to take videos.
|
||||||
|
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// 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 ElementX
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class MediaPickerPreviewScreenScreenUITests: XCTestCase { }
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// 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 XCTest
|
||||||
|
|
||||||
|
@testable import ElementX
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class MediaPickerPreviewScreenScreenViewModelTests: XCTestCase { }
|
||||||
Reference in New Issue
Block a user