#4090 - Add voice message controller, audio recorder and toolbar view links. Working audio file sending and cancellation.

This commit is contained in:
Stefan Ceriu
2021-06-07 10:20:26 +03:00
committed by Stefan Ceriu
parent ad83bbc0f9
commit 08f274bd61
9 changed files with 273 additions and 19 deletions

View File

@@ -67,6 +67,7 @@ typedef enum : NSUInteger
@property (weak, nonatomic) IBOutlet UILabel *inputContextLabel;
@property (weak, nonatomic) IBOutlet UIButton *inputContextButton;
@property (weak, nonatomic) IBOutlet RoomActionsBar *actionsBar;
@property (weak, nonatomic) UIView *voiceMessageToolbarView;
/**
Tell whether the filled data will be sent encrypted. NO by default.

View File

@@ -41,8 +41,6 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
UIAlertController *actionSheet;
}
@property (nonatomic, strong) VoiceMessageToolbarView *voiceMessageToolbarView;
@end
@implementation RoomInputToolbarView
@@ -78,16 +76,19 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
self.isEncryptionEnabled = _isEncryptionEnabled;
self.voiceMessageToolbarView = [VoiceMessageToolbarView instanceFromNib];
[self _updateUIWithTextMessage:nil animated:NO];
}
- (void)setVoiceMessageToolbarView:(UIView *)voiceMessageToolbarView
{
_voiceMessageToolbarView = voiceMessageToolbarView;
self.voiceMessageToolbarView.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:self.voiceMessageToolbarView];
[NSLayoutConstraint activateConstraints:@[[self.mainToolbarView.topAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.topAnchor],
[self.mainToolbarView.leftAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.leftAnchor],
[self.mainToolbarView.bottomAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.bottomAnchor],
[self.mainToolbarView.rightAnchor constraintEqualToAnchor:self.voiceMessageToolbarView.rightAnchor]]];
[self _updateUIWithTextMessage:nil animated:NO];
}
#pragma mark - Override MXKView
@@ -140,8 +141,6 @@ const NSTimeInterval kActionMenuComposerHeightAnimationDuration = .3;
self.inputContextLabel.textColor = ThemeService.shared.theme.textSecondaryColor;
self.inputContextButton.tintColor = ThemeService.shared.theme.textSecondaryColor;
[self.actionsBar updateWithTheme:ThemeService.shared.theme];
[self.voiceMessageToolbarView updateWithTheme:ThemeService.shared.theme];
}
#pragma mark -

View File

@@ -1,146 +0,0 @@
//
// Copyright 2021 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 UIKit
private enum VoiceMessageToolbarViewState {
case idle
case recording
}
class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDelegate {
@IBOutlet private var backgroundView: UIView!
@IBOutlet private var recordButtonsContainerView: UIView!
@IBOutlet private var primaryRecordButton: UIButton!
@IBOutlet private var secondaryRecordButton: UIButton!
@IBOutlet private var slideToCancelContainerView: UIView!
@IBOutlet private var slideToCancelLabel: UILabel!
@IBOutlet private var slideToCancelChevron: UIImageView!
@IBOutlet private var slideToCancelGradient: UIImageView!
private var cancelLabelToRecordButtonDistance: CGFloat = 0.0
private var state: VoiceMessageToolbarViewState = .idle {
didSet {
updateUIAnimated(true)
}
}
private var currentTheme: Theme? {
didSet {
updateUIAnimated(true)
}
}
@objc static func instanceFromNib() -> VoiceMessageToolbarView {
let nib = UINib(nibName: "VoiceMessageToolbarView", bundle: nil)
guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else {
fatalError("The nib \(nib) expected its root view to be of type \(self)")
}
return view
}
override func awakeFromNib() {
super.awakeFromNib()
slideToCancelGradient.image = Asset.Images.voiceMessageCancelGradient.image.withRenderingMode(.alwaysTemplate)
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
longPressGesture.delegate = self
longPressGesture.minimumPressDuration = 0.1
recordButtonsContainerView.addGestureRecognizer(longPressGesture)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
longPressGesture.delegate = self
recordButtonsContainerView.addGestureRecognizer(panGesture)
updateUIAnimated(false)
}
// MARK: - Themable
func update(theme: Theme) {
currentTheme = theme
}
// MARK: - UIGestureRecognizerDelegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
// MARK: - Private
@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
case UIGestureRecognizer.State.ended:
state = .idle
default:
break
}
}
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard self.state == .recording && gestureRecognizer.state == .changed else {
return
}
let translation = gestureRecognizer.translation(in: self)
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)
}
private func updateUIAnimated(_ animated: Bool) {
UIView.animate(withDuration: (animated ? 0.25 : 0.0)) {
switch self.state {
case .idle:
self.slideToCancelContainerView.alpha = 0.0
self.backgroundView.alpha = 0.0
self.slideToCancelGradient.alpha = 0.0
self.recordButtonsContainerView.transform = .identity
self.slideToCancelContainerView.transform = .identity
self.primaryRecordButton.alpha = 1.0
self.secondaryRecordButton.alpha = 0.0
case .recording:
self.slideToCancelContainerView.alpha = 1.0
self.backgroundView.alpha = 1.0
self.slideToCancelGradient.alpha = 1.0
self.primaryRecordButton.alpha = 0.0
self.secondaryRecordButton.alpha = 1.0
}
guard let theme = self.currentTheme else {
return
}
self.backgroundView.backgroundColor = theme.backgroundColor
self.primaryRecordButton.tintColor = theme.textSecondaryColor
self.slideToCancelLabel.textColor = theme.textSecondaryColor
self.slideToCancelChevron.tintColor = theme.textSecondaryColor
self.slideToCancelGradient.tintColor = theme.backgroundColor
}
}
}

View File

@@ -1,113 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="VoiceMessageToolbarView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="40"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="FqE-3x-NQ9">
<rect key="frame" x="0.0" y="0.0" width="414" height="40"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dyu-ha-046" customClass="PassthroughView" customModule="Riot" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="40"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="6FH-4Q-Z5e">
<rect key="frame" x="141" y="10" width="132" height="20.5"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="chevron.left" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="82A-vC-KEp">
<rect key="frame" x="0.0" y="2" width="12.5" height="17"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Slide to cancel" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ydw-Nb-zP6">
<rect key="frame" x="20.5" y="0.0" width="111.5" height="20.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="voice_message_cancel_gradient" translatesAutoresizingMaskIntoConstraints="NO" id="BYJ-HN-opT">
<rect key="frame" x="0.0" y="0.0" width="414" height="40"/>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7OQ-1F-5qT">
<rect key="frame" x="358" y="-6" width="52" height="52"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="BDj-Sw-VQ5">
<rect key="frame" x="0.0" y="0.0" width="52" height="52"/>
<state key="normal" image="voice_message_record_button_default"/>
</button>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rel-Fo-ROL">
<rect key="frame" x="0.0" y="0.0" width="52" height="52"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" image="voice_message_record_button_recording"/>
</button>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="leading" secondItem="7OQ-1F-5qT" secondAttribute="leading" id="2Xv-EI-etf"/>
<constraint firstAttribute="trailing" secondItem="BDj-Sw-VQ5" secondAttribute="trailing" id="2ZQ-3v-0W7"/>
<constraint firstAttribute="height" constant="52" id="4XA-Gb-5NO"/>
<constraint firstAttribute="trailing" secondItem="BDj-Sw-VQ5" secondAttribute="trailing" id="Dki-cT-7xX"/>
<constraint firstAttribute="bottom" secondItem="BDj-Sw-VQ5" secondAttribute="bottom" id="fzv-iX-c1Y"/>
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="top" secondItem="7OQ-1F-5qT" secondAttribute="top" id="mNa-EU-ZKQ"/>
<constraint firstAttribute="bottom" secondItem="BDj-Sw-VQ5" secondAttribute="bottom" id="phX-gD-B2H"/>
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="top" secondItem="7OQ-1F-5qT" secondAttribute="top" id="pv8-li-wP8"/>
<constraint firstItem="BDj-Sw-VQ5" firstAttribute="leading" secondItem="7OQ-1F-5qT" secondAttribute="leading" id="ynJ-4x-1jv"/>
<constraint firstAttribute="width" constant="52" id="zPb-1B-JyA"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="BYJ-HN-opT" firstAttribute="top" secondItem="dyu-ha-046" secondAttribute="top" id="3Mq-1a-iKc"/>
<constraint firstAttribute="trailing" secondItem="7OQ-1F-5qT" secondAttribute="trailing" constant="4" id="7eK-W2-VJy"/>
<constraint firstItem="6FH-4Q-Z5e" firstAttribute="centerX" secondItem="dyu-ha-046" secondAttribute="centerX" id="IZ1-Dr-yrw"/>
<constraint firstItem="6FH-4Q-Z5e" firstAttribute="centerY" secondItem="dyu-ha-046" secondAttribute="centerY" id="NAu-5j-4Yg"/>
<constraint firstAttribute="trailing" secondItem="BYJ-HN-opT" secondAttribute="trailing" id="NWq-wG-sAe"/>
<constraint firstItem="7OQ-1F-5qT" firstAttribute="centerY" secondItem="dyu-ha-046" secondAttribute="centerY" id="cUx-pa-VEf"/>
<constraint firstItem="BYJ-HN-opT" firstAttribute="leading" secondItem="dyu-ha-046" secondAttribute="leading" id="lXc-5e-Ssj"/>
<constraint firstAttribute="bottom" secondItem="BYJ-HN-opT" secondAttribute="bottom" id="yNQ-wC-4iD"/>
</constraints>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="dyu-ha-046" secondAttribute="trailing" id="4OH-8A-tMK"/>
<constraint firstAttribute="bottom" secondItem="dyu-ha-046" secondAttribute="bottom" id="L4o-hW-kta"/>
<constraint firstItem="dyu-ha-046" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="Qmv-hs-X3c"/>
<constraint firstItem="dyu-ha-046" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="Tdp-61-WP9"/>
</constraints>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="backgroundView" destination="FqE-3x-NQ9" id="RFR-SQ-s21"/>
<outlet property="primaryRecordButton" destination="BDj-Sw-VQ5" id="dg3-fG-Bym"/>
<outlet property="recordButtonsContainerView" destination="7OQ-1F-5qT" id="HDQ-r9-2Tu"/>
<outlet property="secondaryRecordButton" destination="rel-Fo-ROL" id="KXM-gt-9hS"/>
<outlet property="slideToCancelChevron" destination="82A-vC-KEp" id="Chg-EH-UBv"/>
<outlet property="slideToCancelContainerView" destination="6FH-4Q-Z5e" id="qCc-rl-vQX"/>
<outlet property="slideToCancelGradient" destination="BYJ-HN-opT" id="qbb-Q9-xSo"/>
<outlet property="slideToCancelLabel" destination="Ydw-Nb-zP6" id="l4Y-Eg-Qwc"/>
</connections>
<point key="canvasLocation" x="-84" y="234"/>
</view>
</objects>
<resources>
<image name="chevron.left" catalog="system" width="96" height="128"/>
<image name="voice_message_cancel_gradient" width="104" height="47"/>
<image name="voice_message_record_button_default" width="22" height="26.5"/>
<image name="voice_message_record_button_recording" width="52" height="52"/>
</resources>
</document>