diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index fb4b7db8f9..9869b17a34 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -135,7 +135,7 @@ @interface RoomViewController () + RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate> { // The preview header @@ -240,6 +240,8 @@ @interface RoomViewController () Void) +} + +public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, AudioRecorderDelegate { + + private let themeService: ThemeService + private let _voiceMessageToolbarView: VoiceMessageToolbarView + + private var audioRecorder: AudioRecorder? + + @objc public weak var delegate: VoiceMessageControllerDelegate? + + @objc public var voiceMessageToolbarView: UIView { + return _voiceMessageToolbarView + } + + @objc public init(themeService: ThemeService) { + _voiceMessageToolbarView = VoiceMessageToolbarView.instanceFromNib() + self.themeService = themeService + + super.init() + + _voiceMessageToolbarView.delegate = self + + self._voiceMessageToolbarView.update(theme: self.themeService.theme) + NotificationCenter.default.addObserver(self, selector: #selector(handleThemeDidChange), name: .themeServiceDidChangeTheme, object: nil) + } + + // MARK: - VoiceMessageToolbarViewDelegate + + func voiceMessageToolbarViewDidRequestRecordingStart(_ toolbarView: VoiceMessageToolbarView) { + let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + let temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString) + + audioRecorder = AudioRecorder() + audioRecorder?.delegate = self + audioRecorder?.recordWithOuputURL(temporaryFileURL) + } + + func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) { + audioRecorder?.stopRecording() + + guard let url = audioRecorder?.url else { + MXLog.error("Invalid audio recording URL") + return + } + + delegate?.voiceMessageController(self, didRequestSendForFileAtURL: url) { [weak self] success in + self?.deleteRecordingAtURL(url) + } + } + + func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) { + audioRecorder?.stopRecording() + deleteRecordingAtURL(audioRecorder?.url) + } + + // MARK: - AudioRecorderDelegate + + func audioRecorderDidStartRecording(_ audioRecorder: AudioRecorder) { + _voiceMessageToolbarView.state = .recording + } + + func audioRecorderDidFinishRecording(_ audioRecorder: AudioRecorder) { + _voiceMessageToolbarView.state = .idle + } + + func audioRecorder(_ audioRecorder: AudioRecorder, didFailWithError: Error) { + MXLog.error("Failed recording voice message.") + _voiceMessageToolbarView.state = .idle + } + + // MARK: - Private + + private func deleteRecordingAtURL(_ url: URL?) { + guard let url = url else { + return + } + + do { + try FileManager.default.removeItem(at: url) + } catch { + MXLog.error(error) + } + } + + @objc private func handleThemeDidChange() { + self._voiceMessageToolbarView.update(theme: self.themeService.theme) + } +} diff --git a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift similarity index 78% rename from Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.swift rename to Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index 959b63a74c..a01a0ff7d4 100644 --- a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -16,12 +16,20 @@ import UIKit -private enum VoiceMessageToolbarViewState { +protocol VoiceMessageToolbarViewDelegate: AnyObject { + func voiceMessageToolbarViewDidRequestRecordingStart(_ toolbarView: VoiceMessageToolbarView) + func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) + func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) +} + +enum VoiceMessageToolbarViewState { case idle case recording } class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDelegate { + + weak var delegate: VoiceMessageToolbarViewDelegate? @IBOutlet private var backgroundView: UIView! @@ -36,14 +44,22 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel private var cancelLabelToRecordButtonDistance: CGFloat = 0.0 - private var state: VoiceMessageToolbarViewState = .idle { + private var currentTheme: Theme? { didSet { updateUIAnimated(true) } } - private var currentTheme: Theme? { + var state: VoiceMessageToolbarViewState = .idle { didSet { + switch state { + case .recording: + let convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView) + cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX + case .idle: + cancelDrag() + } + updateUIAnimated(true) } } @@ -90,13 +106,11 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel @objc private func handleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { switch gestureRecognizer.state { case UIGestureRecognizer.State.began: - state = .recording - - let convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView) - cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX - + delegate?.voiceMessageToolbarViewDidRequestRecordingStart(self) case UIGestureRecognizer.State.ended: - state = .idle + delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) + case UIGestureRecognizer.State.cancelled: + delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self) default: break } @@ -111,6 +125,17 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel recordButtonsContainerView.transform = CGAffineTransform(translationX: min(translation.x, 0.0), y: 0.0) slideToCancelContainerView.transform = CGAffineTransform(translationX: min(translation.x + cancelLabelToRecordButtonDistance, 0.0), y: 0.0) + + if abs(translation.x) > self.bounds.width / 2.0 { + cancelDrag() + } + } + + private func cancelDrag() { + recordButtonsContainerView.gestureRecognizers?.forEach { gestureRecognizer in + gestureRecognizer.isEnabled = false + gestureRecognizer.isEnabled = true + } } private func updateUIAnimated(_ animated: Bool) { diff --git a/Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib similarity index 100% rename from Riot/Modules/Room/Views/InputToolbar/VoiceMessageToolbarView.xib rename to Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib diff --git a/Riot/Utils/PassthroughView.swift b/Riot/Utils/PassthroughView.swift index b101c7ea5f..2d89fc8f7f 100644 --- a/Riot/Utils/PassthroughView.swift +++ b/Riot/Utils/PassthroughView.swift @@ -17,7 +17,7 @@ import UIKit class PassthroughView: UIView { - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let hitTarget = super.hitTest(point, with: event) guard hitTarget == self else {