diff --git a/wire-ios/Wire-iOS.xcodeproj/project.pbxproj b/wire-ios/Wire-iOS.xcodeproj/project.pbxproj index d2439f15ba9..8e89403b7d0 100644 --- a/wire-ios/Wire-iOS.xcodeproj/project.pbxproj +++ b/wire-ios/Wire-iOS.xcodeproj/project.pbxproj @@ -2442,8 +2442,9 @@ "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_COMPILATION_MODE = singlefile; TARGETED_DEVICE_FAMILY = "1,2"; @@ -2459,8 +2460,9 @@ "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -2470,6 +2472,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 01B71A962D6D2DAB002C5A38 /* Tests-Debug.xcconfig */; buildSettings = { + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; @@ -2482,6 +2485,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 01B71A962D6D2DAB002C5A38 /* Tests-Debug.xcconfig */; buildSettings = { + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; diff --git a/wire-ios/Wire-iOS.xcodeproj/xcshareddata/xcschemes/Wire-iOS.xcscheme b/wire-ios/Wire-iOS.xcodeproj/xcshareddata/xcschemes/Wire-iOS.xcscheme index 63412552246..c329d4e05c4 100644 --- a/wire-ios/Wire-iOS.xcodeproj/xcshareddata/xcschemes/Wire-iOS.xcscheme +++ b/wire-ios/Wire-iOS.xcodeproj/xcshareddata/xcschemes/Wire-iOS.xcscheme @@ -76,6 +76,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/wire-ios/Wire-iOS/Sources/Helpers/UIApplication/UIApplication+Permissions.swift b/wire-ios/Wire-iOS/Sources/Helpers/UIApplication/UIApplication+Permissions.swift index 33565167dad..f389f817fae 100644 --- a/wire-ios/Wire-iOS/Sources/Helpers/UIApplication/UIApplication+Permissions.swift +++ b/wire-ios/Wire-iOS/Sources/Helpers/UIApplication/UIApplication+Permissions.swift @@ -55,7 +55,7 @@ extension UIApplication: ApplicationProtocol { } static func wr_requestOrWarnAboutPhotoLibraryAccess(_ grantedHandler: @escaping (Bool) -> Void) { - PHPhotoLibrary.requestAuthorization { status in + PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in DispatchQueue.main.async { switch status { case .restricted: @@ -68,7 +68,7 @@ extension UIApplication: ApplicationProtocol { case .authorized: grantedHandler(true) case .limited: - fallthrough + grantedHandler(true) @unknown default: break } diff --git a/wire-ios/Wire-iOS/Sources/Helpers/syncengine/SessionManager+Convenience.swift b/wire-ios/Wire-iOS/Sources/Helpers/syncengine/SessionManager+Convenience.swift index 62270e162f4..3a09400543b 100644 --- a/wire-ios/Wire-iOS/Sources/Helpers/syncengine/SessionManager+Convenience.swift +++ b/wire-ios/Wire-iOS/Sources/Helpers/syncengine/SessionManager+Convenience.swift @@ -58,7 +58,7 @@ extension SessionManager { let isCallKitEnabled = !isCallKitDisabled let hasAudioPermissions = AVCaptureDevice.authorizationStatus(for: AVMediaType.audio) == AVAuthorizationStatus .authorized - let isCallKitSupported = !UIDevice.isSimulator + let isCallKitSupported = !UIDevice.isSimulator && !ProcessInfo.processInfo.isiOSAppOnMac if isCallKitEnabled, isCallKitSupported, hasAudioPermissions { callNotificationStyle = .callKit diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Camera/CameraController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Camera/CameraController.swift index 98f0efe3fc1..c997663a872 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Camera/CameraController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Camera/CameraController.swift @@ -85,21 +85,35 @@ final class CameraController { // SETUP INPUTS - let availableInputs = [AVCaptureDevice.Position.front, .back] + var availableInputs = [AVCaptureDevice.Position.front, .back] .compactMap { cameraDevice(for: $0) } .compactMap { try? AVCaptureDeviceInput(device: $0) } .filter { session.canAddInput($0) } + // On Mac, builtInWideAngleCamera at front/back positions is unavailable. + // Fall back to the system default video device (e.g. FaceTime HD camera). + if availableInputs.isEmpty, + let defaultDevice = AVCaptureDevice.default(for: .video), + let input = try? AVCaptureDeviceInput(device: defaultDevice), + session.canAddInput(input) { + availableInputs = [input] + } + switch availableInputs.count { case 1: let input = availableInputs.first! - if input.device.position == .front { + switch input.device.position { + case .front: currentCamera = .front frontCameraDeviceInput = input - } else { + case .back: currentCamera = .back backCameraDeviceInput = input + default: + // Mac cameras report .unspecified; treat as front (facing the user). + currentCamera = .front + frontCameraDeviceInput = input } case 2: diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Camera/MacVideoRecorderViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Camera/MacVideoRecorderViewController.swift new file mode 100644 index 00000000000..41c31a51e79 --- /dev/null +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Camera/MacVideoRecorderViewController.swift @@ -0,0 +1,217 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import AVFoundation +import UIKit + +/// A minimal video recorder for Mac (Designed for iPad). +/// UIImagePickerController with .camera source crashes on Mac because Portrait Effects +/// tries to create Metal textures with unsupported IOSurface formats. This replaces it. +final class MacVideoRecorderViewController: UIViewController { + + var maxDuration: TimeInterval = 240 + var onVideoRecorded: ((URL) -> Void)? + + private let session = AVCaptureSession() + private let sessionQueue = DispatchQueue(label: "com.wire.mac-video-recorder.session") + private let movieOutput = AVCaptureMovieFileOutput() + private var previewLayer: AVCaptureVideoPreviewLayer! + private var recordingTimer: Timer? + private var elapsedSeconds = 0 + + private lazy var recordButton: UIButton = { + var config = UIButton.Configuration.filled() + config.cornerStyle = .capsule + config.baseForegroundColor = .white + config.baseBackgroundColor = .systemRed + config.contentInsets = NSDirectionalEdgeInsets(top: 14, leading: 28, bottom: 14, trailing: 28) + let button = UIButton(configuration: config) + button.setTitle(L10n.Localizable.Content.File.takeVideo, for: .normal) + button.setTitle("Stop", for: .selected) + button.addTarget(self, action: #selector(toggleRecording), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private lazy var cancelButton: UIButton = { + var config = UIButton.Configuration.plain() + config.baseForegroundColor = .white + let button = UIButton(configuration: config) + button.setTitle(L10n.Localizable.General.cancel, for: .normal) + button.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private lazy var timerLabel: UILabel = { + let label = UILabel() + label.textColor = .white + label.font = .monospacedDigitSystemFont(ofSize: 17, weight: .semibold) + label.text = formatTime(0) + label.translatesAutoresizingMaskIntoConstraints = false + label.isHidden = true + return label + }() + + private lazy var recordingIndicator: UIView = { + let view = UIView() + view.backgroundColor = .systemRed + view.layer.cornerRadius = 5 + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + NSLayoutConstraint.activate([ + view.widthAnchor.constraint(equalToConstant: 10), + view.heightAnchor.constraint(equalToConstant: 10) + ]) + return view + }() + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .black + setupSession() + setupUI() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + sessionQueue.async { self.session.startRunning() } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + if movieOutput.isRecording { movieOutput.stopRecording() } + sessionQueue.async { self.session.stopRunning() } + recordingTimer?.invalidate() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + previewLayer.frame = view.bounds + } + + private func setupSession() { + session.beginConfiguration() + // .medium avoids high-quality video configurations that tend to trigger Portrait Effects + session.sessionPreset = .medium + session.automaticallyConfiguresCaptureDeviceForWideColor = false + + if let device = AVCaptureDevice.default(for: .video), + let input = try? AVCaptureDeviceInput(device: device), + session.canAddInput(input) { + session.addInput(input) + } + + if let device = AVCaptureDevice.default(for: .audio), + let input = try? AVCaptureDeviceInput(device: device), + session.canAddInput(input) { + session.addInput(input) + } + + if session.canAddOutput(movieOutput) { + session.addOutput(movieOutput) + } + + session.commitConfiguration() + + previewLayer = AVCaptureVideoPreviewLayer(session: session) + previewLayer.videoGravity = .resizeAspectFill + view.layer.insertSublayer(previewLayer, at: 0) + } + + private func setupUI() { + let timerStack = UIStackView(arrangedSubviews: [recordingIndicator, timerLabel]) + timerStack.axis = .horizontal + timerStack.spacing = 6 + timerStack.alignment = .center + timerStack.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(timerStack) + + view.addSubview(cancelButton) + view.addSubview(recordButton) + + NSLayoutConstraint.activate([ + timerStack.centerXAnchor.constraint(equalTo: view.centerXAnchor), + timerStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + + recordButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + recordButton.bottomAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.bottomAnchor, + constant: -28 + ), + + cancelButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + cancelButton.centerYAnchor.constraint(equalTo: recordButton.centerYAnchor) + ]) + } + + @objc private func toggleRecording() { + if movieOutput.isRecording { + movieOutput.stopRecording() + recordButton.isSelected = false + recordingTimer?.invalidate() + } else { + let url = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent(UUID().uuidString) + .appendingPathExtension("mov") + movieOutput.maxRecordedDuration = CMTime(seconds: maxDuration, preferredTimescale: 600) + movieOutput.startRecording(to: url, recordingDelegate: self) + recordButton.isSelected = true + elapsedSeconds = 0 + timerLabel.isHidden = false + recordingIndicator.isHidden = false + timerLabel.text = formatTime(0) + recordingTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + guard let self else { return } + self.elapsedSeconds += 1 + self.timerLabel.text = self.formatTime(self.elapsedSeconds) + } + } + } + + @objc private func cancelTapped() { + if movieOutput.isRecording { movieOutput.stopRecording() } + dismiss(animated: true) + } + + private func formatTime(_ seconds: Int) -> String { + String(format: "%d:%02d", seconds / 60, seconds % 60) + } +} + +extension MacVideoRecorderViewController: AVCaptureFileOutputRecordingDelegate { + + func fileOutput( + _ output: AVCaptureFileOutput, + didFinishRecordingTo outputFileURL: URL, + from connections: [AVCaptureConnection], + error: Error? + ) { + DispatchQueue.main.async { + self.recordButton.isSelected = false + self.recordingTimer?.invalidate() + self.recordingIndicator.isHidden = true + + guard error == nil else { return } + + self.dismiss(animated: true) { + self.onVideoRecorded?(outputFileURL) + } + } + } +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Components/Views/LinkInteractionTextView.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Components/Views/LinkInteractionTextView.swift index d30ae0183e9..c43b2c5685d 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Components/Views/LinkInteractionTextView.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Components/Views/LinkInteractionTextView.swift @@ -28,8 +28,17 @@ final class LinkInteractionTextView: UITextView { weak var interactionDelegate: TextViewInteractionDelegate? override var selectedTextRange: UITextRange? { - get { nil } - set { /* no-op */ } + get { + guard !ProcessInfo.processInfo.isiOSAppOnMac else { return super.selectedTextRange } + return nil + } + set { + guard !ProcessInfo.processInfo.isiOSAppOnMac else { + super.selectedTextRange = newValue + return + } + // no-op on iOS: prevents accidental text selection when tapping a message + } } // URLs with these schemes should be handled by the os. @@ -51,6 +60,8 @@ final class LinkInteractionTextView: UITextView { } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + // On macOS let the text view receive all hits so text can be selected with cursor. + guard !ProcessInfo.processInfo.isiOSAppOnMac else { return super.point(inside: point, with: event) } let isInside = super.point(inside: point, with: event) guard !UIMenuController.shared.isMenuVisible else { return false } guard let position = characterRange(at: point), isInside else { return false } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Text/ConversationTextMessageCell.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Text/ConversationTextMessageCell.swift index 0e7242e04bf..f31626b4025 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Text/ConversationTextMessageCell.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/Content/Text/ConversationTextMessageCell.swift @@ -148,6 +148,13 @@ final class ConversationTextMessageCell: UIView, ConversationMessageCell, TextVi messageTextView.textColor = textForegroundColor messageTextView.linkTextAttributes = linkTextAttributes + + if ProcessInfo.processInfo.isiOSAppOnMac { + // On macOS the cursor and selection use tintColor, which inherits the app accent color. + // For own messages the bubble background IS the accent color, making the cursor invisible. + // Use the text foreground color instead so the cursor always contrasts with the bubble. + messageTextView.tintColor = textForegroundColor + } } func configure(with object: Configuration, animated: Bool) { diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/ConversationMessageCellTableViewAdapter.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/ConversationMessageCellTableViewAdapter.swift index fb34c5ea051..c3834cfba34 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/ConversationMessageCellTableViewAdapter.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Content/Cells/ConfigurationMessageCell/ConversationMessageCellTableViewAdapter.swift @@ -293,6 +293,7 @@ final class ConversationMessageCellTableViewAdapter< verticalFittingPriority: verticalFittingPriority ) } + } extension UITableView { diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/CameraKeyboard/PhotoPermissionsControllerStrategy.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/CameraKeyboard/PhotoPermissionsControllerStrategy.swift index bba7d9910b8..866313cd92e 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/CameraKeyboard/PhotoPermissionsControllerStrategy.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/CameraKeyboard/PhotoPermissionsControllerStrategy.swift @@ -33,7 +33,7 @@ final class PhotoPermissionsControllerStrategy: PhotoPermissionsController { var isPhotoLibraryAuthorized: Bool { switch PHPhotoLibrary.authorizationStatus() { - case .authorized: true + case .authorized, .limited: true default: false } } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Camera.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Camera.swift index cedfe9757d3..c8619c6a210 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Camera.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Camera.swift @@ -18,7 +18,8 @@ import FLAnimatedImage import MobileCoreServices -import Photos +import PhotosUI +import UniformTypeIdentifiers import WireCommonComponents import WireLogging import WireReusableUIComponents @@ -220,7 +221,7 @@ extension ConversationInputBarViewController: CameraKeyboardViewControllerDelega PHPhotoLibrary.requestAuthorization { status in DispatchQueue.main.async { switch status { - case .authorized: + case .authorized, .limited: closure(true) default: closure(false) @@ -346,6 +347,19 @@ extension ConversationInputBarViewController: CanvasViewControllerDelegate { extension ConversationInputBarViewController { func showCameraAndPhotos() { + // On Mac, both CameraKeyboardViewController and PHPickerViewController connect to + // PHPhotoLibrary via XPC, which hangs when the library is in a cloud-synced path. + // UIDocumentPickerViewController reads directly from the filesystem and avoids + // Photos Library entirely. + if ProcessInfo.processInfo.isiOSAppOnMac { + let types: [UTType] = [.image, .jpeg, .png, .heic, .gif, .movie, .video, .mpeg4Movie] + let picker = UIDocumentPickerViewController(forOpeningContentTypes: types) + picker.allowsMultipleSelection = false + picker.delegate = macMediaPickerCoordinator + present(picker, animated: true) + return + } + UIApplication.wr_requestVideoAccess { [mediaShareRestrictionManager] _ in if SecurityFlags.cameraRoll.isEnabled, mediaShareRestrictionManager.hasAccessToCameraRoll { @@ -380,3 +394,50 @@ extension ConversationInputBarViewController { } } } + +// MARK: - Mac media picker + +/// Handles UIDocumentPickerViewController for the camera button on Mac. +/// A separate delegate is used so it doesn't conflict with the file-upload +/// UIDocumentPickerDelegate already conforming to ConversationInputBarViewController. +private final class MacMediaPickerCoordinator: NSObject, UIDocumentPickerDelegate { + + weak var inputBar: ConversationInputBarViewController? + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + guard let vc = inputBar, let url = urls.first else { return } + + let type = UTType(filenameExtension: url.pathExtension) + if type?.conforms(to: .audiovisualContent) == true || type?.conforms(to: .movie) == true { + vc.processRecordedVideoAt(url) + } else if type?.conforms(to: .image) == true { + guard let data = try? Data(contentsOf: url), + let image = UIImage(data: data), + let jpegData = image.jpegData(compressionQuality: 0.9) + else { return } + let sendable = SendableImage(name: nil, utType: .jpeg, data: jpegData) + vc.showConfirmationForImage(sendable, isFromCamera: false) + } + } +} + +private var macMediaPickerCoordinatorKey: UInt8 = 0 + +extension ConversationInputBarViewController { + + fileprivate var macMediaPickerCoordinator: MacMediaPickerCoordinator { + if let existing = objc_getAssociatedObject(self, &macMediaPickerCoordinatorKey) + as? MacMediaPickerCoordinator { + return existing + } + let coordinator = MacMediaPickerCoordinator() + coordinator.inputBar = self + objc_setAssociatedObject( + self, + &macMediaPickerCoordinatorKey, + coordinator, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + return coordinator + } +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+ImagePicker.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+ImagePicker.swift index 807f5d0c31b..ecc65a909a0 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+ImagePicker.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+ImagePicker.swift @@ -31,6 +31,21 @@ extension ConversationInputBarViewController { pointToView: UIView ) { + // UIImagePickerController with .camera crashes on Mac (Designed for iPad) because Portrait + // Effects initializes Metal textures with unsupported IOSurface formats. Use the custom + // Mac recorder instead. + if ProcessInfo.processInfo.isiOSAppOnMac && sourceType == .camera { + execute(videoPermissions: { [self] in + let recorder = MacVideoRecorderViewController() + recorder.maxDuration = userSession.maxVideoLength + recorder.onVideoRecorded = { [weak self] url in + self?.processRecordedVideoAt(url) + } + present(recorder, animated: true) + }) + return + } + if !UIImagePickerController.isSourceTypeAvailable(sourceType) { if UIDevice.isSimulator { let testFilePath = "/var/tmp/video.mp4" @@ -39,7 +54,6 @@ extension ConversationInputBarViewController { } } return - // Don't crash on Simulator } let presentController = { [self] in @@ -127,4 +141,37 @@ extension ConversationInputBarViewController { } } + /// Processes a video recorded by `MacVideoRecorderViewController`. + /// Skips the camera-roll save step that doesn't apply on Mac. + func processRecordedVideoAt(_ videoURL: URL) { + guard let selfUser = ZMUser.selfUser() else { + assertionFailure("ZMUser.selfUser() is nil") + return + } + + let videoTempURL = URL( + fileURLWithPath: NSTemporaryDirectory(), + isDirectory: true + ).appendingPathComponent(String.filename(for: selfUser)) + .appendingPathExtension(videoURL.pathExtension) + + do { + try FileManager.default.removeTmpIfNeededAndCopy(fileURL: videoURL, tmpURL: videoTempURL) + } catch { + zmLog.error("Cannot copy video from \(videoURL) to \(videoTempURL): \(error)") + return + } + + AVURLAsset + .convertVideoToUploadFormat( + at: videoTempURL, + fileLengthLimit: Int64(userSession.maxUploadFileSize) + ) { [weak self] resultURL, _, error in + guard let self else { return } + if error == nil, let resultURL { + self.uploadFiles(at: [resultURL]) + } + } + } + } diff --git a/wire-ios/Wire-iOS/Wire-Info.plist b/wire-ios/Wire-iOS/Wire-Info.plist index a5ee22a2c47..47bd5669223 100644 --- a/wire-ios/Wire-iOS/Wire-Info.plist +++ b/wire-ios/Wire-iOS/Wire-Info.plist @@ -154,7 +154,7 @@ UIRequiredDeviceCapabilities UIRequiresFullScreen - + UIStatusBarHidden UISupportedInterfaceOrientations