diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_delete.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_delete.imageset/Contents.json index bb4344e92..a255919c5 100644 --- a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_delete.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_delete.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/Contents.json new file mode 100644 index 000000000..6b40d80e4 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "voice_message_lock_chevron.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_lock_chevron@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_lock_chevron@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron.png new file mode 100644 index 000000000..332200ef8 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron@2x.png new file mode 100644 index 000000000..ff29e0808 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron@3x.png new file mode 100644 index 000000000..b9f5c04d5 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_chevron.imageset/voice_message_lock_chevron@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/Contents.json new file mode 100644 index 000000000..93de75077 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "voice_message_lock_icon_locked.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_lock_icon_locked@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_lock_icon_locked@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked.png new file mode 100644 index 000000000..bb79d7785 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked@2x.png new file mode 100644 index 000000000..521e0df1f Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked@3x.png new file mode 100644 index 000000000..b7cdaf911 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_locked.imageset/voice_message_lock_icon_locked@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/Contents.json new file mode 100644 index 000000000..43a311cf4 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "voice_message_lock_icon_unlocked.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "voice_message_lock_icon_unlocked@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "voice_message_lock_icon_unlocked@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked.png new file mode 100644 index 000000000..91b915c03 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked@2x.png new file mode 100644 index 000000000..4a50acc94 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked@3x.png new file mode 100644 index 000000000..4264165d4 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_lock_icon_unlocked.imageset/voice_message_lock_icon_unlocked@3x.png differ diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index c3b0bb50e..aab42d27e 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -131,6 +131,9 @@ internal enum Asset { internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon") internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon") internal static let voiceMessageCancelGradient = ImageAsset(name: "voice_message_cancel_gradient") + internal static let voiceMessageLockChevron = ImageAsset(name: "voice_message_lock_chevron") + internal static let voiceMessageLockIconLocked = ImageAsset(name: "voice_message_lock_icon_locked") + internal static let voiceMessageLockIconUnlocked = ImageAsset(name: "voice_message_lock_icon_unlocked") internal static let voiceMessagePauseButtonDark = ImageAsset(name: "voice_message_pause_button_dark") internal static let voiceMessagePauseButtonLight = ImageAsset(name: "voice_message_pause_button_light") internal static let voiceMessagePlayButtonDark = ImageAsset(name: "voice_message_play_button_dark") diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift index e639c6ce6..635e5ea40 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAudioRecorder.swift @@ -17,6 +17,8 @@ import Foundation import AVFoundation +private let silenceThreshold: Float = -50.0 + protocol VoiceMessageAudioRecorderDelegate: AnyObject { func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) func audioRecorderDidFinishRecording(_ audioRecorder: VoiceMessageAudioRecorder) @@ -104,11 +106,7 @@ class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate { } private func normalizedPowerLevelFromDecibels(_ decibels: Float) -> Float { - if decibels < -60.0 || decibels == 0.0 { - return 0.0 - } - - return powf((powf(10.0, 0.05 * decibels) - powf(10.0, 0.05 * -60.0)) * (1.0 / (1.0 - powf(10.0, 0.05 * -60.0))), 1.0 / 2.0) + return decibels / silenceThreshold } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index 94306a5ae..b00d68d82 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -31,6 +31,9 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, private var audioRecorder: VoiceMessageAudioRecorder? + private var audioSamples: [Float] = [] + private var isInLockedMode: Bool = false + @objc public weak var delegate: VoiceMessageControllerDelegate? @objc public var voiceMessageToolbarView: UIView { @@ -54,6 +57,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, self._voiceMessageToolbarView.update(theme: self.themeService.theme) NotificationCenter.default.addObserver(self, selector: #selector(handleThemeDidChange), name: .themeServiceDidChangeTheme, object: nil) + + updateUI() } // MARK: - VoiceMessageToolbarViewDelegate @@ -87,27 +92,32 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) { + isInLockedMode = false audioRecorder?.stopRecording() deleteRecordingAtURL(audioRecorder?.url) UINotificationFeedbackGenerator().notificationOccurred(.error) } + func voiceMessageToolbarViewDidRequestLockedModeRecording(_ toolbarView: VoiceMessageToolbarView) { + isInLockedMode = true + updateUI() + } + // MARK: - AudioRecorderDelegate func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder) { - _voiceMessageToolbarView.state = .recording - self.displayLink.isPaused = false + updateUI() } func audioRecorderDidFinishRecording(_ audioRecorder: VoiceMessageAudioRecorder) { - _voiceMessageToolbarView.state = .idle - displayLink.isPaused = true + updateUI() } func audioRecorder(_ audioRecorder: VoiceMessageAudioRecorder, didFailWithError: Error) { + isInLockedMode = false + updateUI() + MXLog.error("Failed recording voice message.") - _voiceMessageToolbarView.state = .idle - displayLink.isPaused = true } // MARK: - Private @@ -129,10 +139,27 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate, } @objc private func handleDisplayLinkTick() { - guard let audioRecorder = audioRecorder else { - return + updateUI() + } + + private func updateUI() { + displayLink.isPaused = !(audioRecorder?.isRecording ?? false) + + let requiredNumberOfSamples = _voiceMessageToolbarView.getRequiredNumberOfSamples() + + if audioSamples.count != requiredNumberOfSamples { + audioSamples = [Float](repeating: 0.0, count: requiredNumberOfSamples) } - _voiceMessageToolbarView.elapsedTime = timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioRecorder.currentTime)) + if let sample = audioRecorder?.averagePowerForChannelNumber(0) { + audioSamples.append(sample) + audioSamples.remove(at: 0) + } + + var details = VoiceMessageToolbarViewDetails() + details.state = (audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle)) + details.elapsedTime = timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: audioRecorder?.currentTime ?? 0.0)) + details.audioSamples = audioSamples + _voiceMessageToolbarView.configureWithDetails(details) } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift index 7e9ddce0d..ba0bd2654 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift @@ -26,6 +26,7 @@ struct VoiceMessagePlaybackViewDetails { var samples: [Float] = [] var playing: Bool = false var playbackEnabled = false + var recording: Bool = false } class VoiceMessagePlaybackView: UIView { @@ -33,6 +34,7 @@ class VoiceMessagePlaybackView: UIView { private var waveformView: VoiceMessageWaveformView! @IBOutlet private var backgroundView: UIView! + @IBOutlet private var recordingIcon: UIView! @IBOutlet private var playButton: UIButton! @IBOutlet private var elapsedTimeLabel: UILabel! @IBOutlet private var waveformContainerView: UIView! @@ -66,6 +68,8 @@ class VoiceMessagePlaybackView: UIView { } playButton.isEnabled = details.playbackEnabled + playButton.isHidden = details.recording + recordingIcon.isHidden = !details.recording elapsedTimeLabel.text = details.currentTime waveformView.progress = details.progress diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib index 545fab174..c49272d99 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib @@ -21,44 +21,48 @@ - - - - - - + - - + - - - - - - + - - - + + @@ -68,6 +72,7 @@ + @@ -75,5 +80,6 @@ + diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift index 8f7b664f9..d46493b81 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.swift @@ -20,16 +20,27 @@ protocol VoiceMessageToolbarViewDelegate: AnyObject { func voiceMessageToolbarViewDidRequestRecordingStart(_ toolbarView: VoiceMessageToolbarView) func voiceMessageToolbarViewDidRequestRecordingCancel(_ toolbarView: VoiceMessageToolbarView) func voiceMessageToolbarViewDidRequestRecordingFinish(_ toolbarView: VoiceMessageToolbarView) + func voiceMessageToolbarViewDidRequestLockedModeRecording(_ toolbarView: VoiceMessageToolbarView) } enum VoiceMessageToolbarViewUIState { case idle - case recording + case record + case lockedModeRecord + case lockedModePlayback +} + +struct VoiceMessageToolbarViewDetails { + var state: VoiceMessageToolbarViewUIState = .idle + var elapsedTime: String = "" + var audioSamples: [Float] = [] } class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDelegate { @IBOutlet private var backgroundView: UIView! + @IBOutlet private var recordingContainerView: UIView! + @IBOutlet private var recordButtonsContainerView: UIView! @IBOutlet private var primaryRecordButton: UIButton! @IBOutlet private var secondaryRecordButton: UIButton! @@ -37,43 +48,40 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel @IBOutlet private var recordingChromeContainerView: UIView! @IBOutlet private var recordingIndicatorView: UIView! + @IBOutlet private var elapsedTimeLabel: UILabel! + @IBOutlet private var slideToCancelContainerView: UIView! @IBOutlet private var slideToCancelLabel: UILabel! @IBOutlet private var slideToCancelChevron: UIImageView! @IBOutlet private var slideToCancelGradient: UIImageView! - @IBOutlet private var elapsedTimeLabel: UILabel! + @IBOutlet private var lockContainerView: UIView! + @IBOutlet private var lockContainerBackgroundView: UIView! + @IBOutlet private var primaryLockButton: UIButton! + @IBOutlet private var secondaryLockButton: UIButton! + @IBOutlet private var lockChevron: UIView! + + @IBOutlet private var lockedModeContainerView: UIView! + @IBOutlet private var deleteButton: UIButton! + @IBOutlet private var playbackViewContainerView: UIView! + @IBOutlet private var sendButton: UIButton! + + private var playbackView: VoiceMessagePlaybackView! private var cancelLabelToRecordButtonDistance: CGFloat = 0.0 + private var lockChevronToRecordButtonDistance: CGFloat = 0.0 + private var lockChevronToLockButtonDistance: CGFloat = 0.0 + private var panDirection: UISwipeGestureRecognizer.Direction? + + private var details: VoiceMessageToolbarViewDetails? private var currentTheme: Theme? { didSet { - updateUIAnimated(true) + updateUIWithDetails(details, animated: true) } } weak var delegate: VoiceMessageToolbarViewDelegate? - - var state: VoiceMessageToolbarViewUIState = .idle { - didSet { - switch state { - case .recording: - let convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView) - cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX - startAnimatingRecordingIndicator() - case .idle: - cancelDrag() - } - - updateUIAnimated(true) - } - } - - var elapsedTime: String? { - didSet { - elapsedTimeLabel.text = elapsedTime - } - } @objc static func instanceFromNib() -> VoiceMessageToolbarView { let nib = UINib(nibName: "VoiceMessageToolbarView", bundle: nil) @@ -87,6 +95,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel super.awakeFromNib() slideToCancelGradient.image = Asset.Images.voiceMessageCancelGradient.image.withRenderingMode(.alwaysTemplate) + lockContainerBackgroundView.layer.cornerRadius = lockContainerBackgroundView.bounds.width / 2.0 let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) longPressGesture.delegate = self @@ -97,7 +106,52 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel longPressGesture.delegate = self recordButtonsContainerView.addGestureRecognizer(panGesture) - updateUIAnimated(false) + playbackView = VoiceMessagePlaybackView.instanceFromNib() + playbackViewContainerView.vc_addSubViewMatchingParent(playbackView) + + updateUIWithDetails(VoiceMessageToolbarViewDetails(), animated: false) + } + + func configureWithDetails(_ details: VoiceMessageToolbarViewDetails) { + elapsedTimeLabel.text = details.elapsedTime + + UIView.animate(withDuration: 0.25) { + self.updatePlaybackViewWithDetails(details) + } + + if self.details?.state != details.state { + switch details.state { + case .record: + var convertedFrame = self.convert(slideToCancelLabel.frame, from: slideToCancelContainerView) + cancelLabelToRecordButtonDistance = recordButtonsContainerView.frame.minX - convertedFrame.maxX + + convertedFrame = self.convert(lockChevron.frame, from: lockContainerView) + lockChevronToRecordButtonDistance = recordButtonsContainerView.frame.midY + convertedFrame.maxY + + lockChevronToLockButtonDistance = lockChevron.frame.minY - primaryLockButton.frame.midY + + startAnimatingRecordingIndicator() + default: + cancelDrag() + } + + if details.state == .lockedModeRecord && self.details?.state == .record { + UIView.animate(withDuration: 0.25) { + self.secondaryLockButton.transform = CGAffineTransform(scaleX: 0.1, y: 0.1) + self.secondaryLockButton.alpha = 0.0 + } completion: { _ in + self.updateUIWithDetails(details, animated: true) + } + } else { + updateUIWithDetails(details, animated: true) + } + } + + self.details = details + } + + func getRequiredNumberOfSamples() -> Int { + return playbackView.getRequiredNumberOfSamples() } // MARK: - Themable @@ -119,8 +173,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel case UIGestureRecognizer.State.began: delegate?.voiceMessageToolbarViewDidRequestRecordingStart(self) case UIGestureRecognizer.State.ended: - delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) - case UIGestureRecognizer.State.cancelled: + // delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self) default: break @@ -128,17 +181,49 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel } @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { - guard self.state == .recording && gestureRecognizer.state == .changed else { + guard details?.state == .record && gestureRecognizer.state == .changed else { return } let translation = gestureRecognizer.translation(in: self) - secondaryRecordButton.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) <= 20.0 && abs(translation.y) <= 20.0 { + panDirection = nil + } else if panDirection == nil { + if abs(translation.x) >= abs(translation.y) { + panDirection = .left + } else { + panDirection = .up + } + } - if abs(translation.x - recordButtonsContainerView.frame.width / 2.0) > self.bounds.width / 2.0 { - cancelDrag() + if panDirection == .left { + secondaryRecordButton.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 - recordButtonsContainerView.frame.width / 2.0) > self.bounds.width / 2.0 { + delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self) + } + } else if panDirection == .up { + secondaryRecordButton.transform = CGAffineTransform(translationX: 0.0, y: min(0.0, translation.y)) + + let yTranslation = min(max(translation.y + lockChevronToRecordButtonDistance, -lockChevronToLockButtonDistance), 0.0) + lockChevron.transform = CGAffineTransform(translationX: 0.0, y: yTranslation) + + let transitionPercentage = abs(yTranslation) / lockChevronToLockButtonDistance + + lockChevron.alpha = 1.0 - transitionPercentage + secondaryRecordButton.alpha = 1.0 - transitionPercentage + primaryLockButton.alpha = 1.0 - transitionPercentage + lockContainerBackgroundView.alpha = 1.0 - transitionPercentage + secondaryLockButton.alpha = transitionPercentage + + if transitionPercentage >= 1.0 { + self.delegate?.voiceMessageToolbarViewDidRequestLockedModeRecording(self) + } + + } else { + secondaryRecordButton.transform = CGAffineTransform(translationX: min(0.0, translation.x), y: min(0.0, translation.y)) } } @@ -149,19 +234,42 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel } } - private func updateUIAnimated(_ animated: Bool) { - UIView.animate(withDuration: (animated ? 0.25 : 0.0)) { - switch self.state { - case .idle: - self.backgroundView.alpha = 0.0 - self.primaryRecordButton.alpha = 1.0 - self.secondaryRecordButton.alpha = 0.0 - self.recordingChromeContainerView.alpha = 0.0 - case .recording: + private func updateUIWithDetails(_ details: VoiceMessageToolbarViewDetails?, animated: Bool) { + guard let details = details else { + return + } + + UIView.animate(withDuration: (animated ? 0.25 : 0.0), delay: 0.0, options: .beginFromCurrentState) { + switch details.state { + case .record: self.backgroundView.alpha = 1.0 self.primaryRecordButton.alpha = 0.0 self.secondaryRecordButton.alpha = 1.0 self.recordingChromeContainerView.alpha = 1.0 + self.lockContainerView.alpha = 1.0 + self.lockContainerBackgroundView.alpha = 1.0 + self.lockedModeContainerView.alpha = 0.0 + self.recordingContainerView.alpha = 1.0 + case .lockedModeRecord: + self.backgroundView.alpha = 1.0 + self.primaryRecordButton.alpha = 0.0 + self.secondaryRecordButton.alpha = 0.0 + self.recordingChromeContainerView.alpha = 0.0 + self.lockContainerView.alpha = 0.0 + self.lockedModeContainerView.alpha = 1.0 + self.recordingContainerView.alpha = 0.0 + default: + self.backgroundView.alpha = 0.0 + self.primaryRecordButton.alpha = 1.0 + self.secondaryRecordButton.alpha = 0.0 + self.recordingChromeContainerView.alpha = 0.0 + self.lockContainerView.alpha = 0.0 + self.lockContainerBackgroundView.alpha = 1.0 + self.primaryLockButton.alpha = 1.0 + self.secondaryLockButton.alpha = 0.0 + self.lockChevron.alpha = 1.0 + self.lockedModeContainerView.alpha = 0.0 + self.recordingContainerView.alpha = 1.0 } guard let theme = self.currentTheme else { @@ -176,18 +284,30 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel self.slideToCancelChevron.tintColor = theme.textSecondaryColor self.elapsedTimeLabel.textColor = theme.textSecondaryColor } completion: { _ in - switch self.state { + switch details.state { case .idle: self.secondaryRecordButton.transform = .identity self.slideToCancelContainerView.transform = .identity + self.lockChevron.transform = .identity + self.secondaryLockButton.transform = .identity default: break } } } + private func updatePlaybackViewWithDetails(_ details: VoiceMessageToolbarViewDetails) { + var playbackViewDetails = VoiceMessagePlaybackViewDetails() + playbackViewDetails.recording = (details.state == .record || details.state == .lockedModeRecord) + playbackViewDetails.currentTime = details.elapsedTime + playbackViewDetails.samples = details.audioSamples + playbackViewDetails.playbackEnabled = true + playbackViewDetails.progress = 0.0 + playbackView.configureWithDetails(playbackViewDetails) + } + private func startAnimatingRecordingIndicator() { - if self.state != .recording { + if self.details?.state != .record { return } @@ -202,4 +322,12 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel } } + + @IBAction private func onTrashButtonTap(_ sender: UIBarItem) { + delegate?.voiceMessageToolbarViewDidRequestRecordingCancel(self) + } + + @IBAction private func onSendButtonTap(_ sender: UIBarItem) { + delegate?.voiceMessageToolbarViewDidRequestRecordingFinish(self) + } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib index 6218ad96a..fe548450b 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib @@ -11,121 +11,236 @@ - + - + - - + + - - + + - - + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + - + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + - - + + - - + + + + + + + + + + + + + + - - - - - - - - - - + + + + - - - - - - + + + + + + + + + + + + + + + + + + - + + + + + + diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift index 39c159e04..8a7a65dff 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageWaveformView.swift @@ -56,11 +56,6 @@ class VoiceMessageWaveformView: UIView { updateBarViews() } - func addSample(_ sample: Float) { - samples.append(sample) - updateBarViews() - } - // MARK: - Private private func setupBarViews() {