mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-25 19:10:49 +02:00
#4094 - Various tweaks, bug fixes and improvements following code review.
This commit is contained in:
@@ -17,8 +17,6 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
private let silenceThreshold: Float = -50.0
|
||||
|
||||
protocol VoiceMessageAudioRecorderDelegate: AnyObject {
|
||||
func audioRecorderDidStartRecording(_ audioRecorder: VoiceMessageAudioRecorder)
|
||||
func audioRecorderDidFinishRecording(_ audioRecorder: VoiceMessageAudioRecorder)
|
||||
@@ -31,6 +29,10 @@ enum VoiceMessageAudioRecorderError: Error {
|
||||
|
||||
class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate {
|
||||
|
||||
private enum Constants {
|
||||
static let silenceThreshold: Float = -50.0
|
||||
}
|
||||
|
||||
private var audioRecorder: AVAudioRecorder?
|
||||
|
||||
var url: URL? {
|
||||
@@ -105,7 +107,7 @@ class VoiceMessageAudioRecorder: NSObject, AVAudioRecorderDelegate {
|
||||
}
|
||||
|
||||
private func normalizedPowerLevelFromDecibels(_ decibels: Float) -> Float {
|
||||
return decibels / silenceThreshold
|
||||
return decibels / Constants.silenceThreshold
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
}
|
||||
|
||||
@objc public init(themeService: ThemeService) {
|
||||
_voiceMessageToolbarView = VoiceMessageToolbarView.instanceFromNib()
|
||||
_voiceMessageToolbarView = VoiceMessageToolbarView.loadFromNib()
|
||||
self.themeService = themeService
|
||||
self.timeFormatter = DateFormatter()
|
||||
|
||||
@@ -59,8 +59,8 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
displayLink.isPaused = true
|
||||
displayLink.add(to: .current, forMode: .common)
|
||||
|
||||
_voiceMessageToolbarView.update(theme: themeService.theme)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleThemeDidChange), name: .themeServiceDidChangeTheme, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateTheme), name: .themeServiceDidChangeTheme, object: nil)
|
||||
updateTheme()
|
||||
|
||||
updateUI()
|
||||
}
|
||||
@@ -197,7 +197,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleThemeDidChange() {
|
||||
@objc private func updateTheme() {
|
||||
_voiceMessageToolbarView.update(theme: themeService.theme)
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
let requiredNumberOfSamples = _voiceMessageToolbarView.getRequiredNumberOfSamples()
|
||||
|
||||
if audioSamples.count != requiredNumberOfSamples {
|
||||
audioSamples = [Float](repeating: 0.0, count: requiredNumberOfSamples)
|
||||
padSamplesArrayToSize(requiredNumberOfSamples)
|
||||
}
|
||||
|
||||
let sample = audioRecorder?.averagePowerForChannelNumber(0) ?? 0.0
|
||||
@@ -250,7 +250,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
|
||||
let requiredNumberOfSamples = _voiceMessageToolbarView.getRequiredNumberOfSamples()
|
||||
if audioSamples.count != requiredNumberOfSamples {
|
||||
audioSamples = [Float](repeating: 0.0, count: requiredNumberOfSamples)
|
||||
padSamplesArrayToSize(requiredNumberOfSamples)
|
||||
|
||||
waveformAnalyser = WaveformAnalyzer(audioAssetURL: url)
|
||||
waveformAnalyser?.samples(count: requiredNumberOfSamples, completionHandler: { [weak self] samples in
|
||||
@@ -274,4 +274,13 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
details.progress = (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0)
|
||||
_voiceMessageToolbarView.configureWithDetails(details)
|
||||
}
|
||||
|
||||
private func padSamplesArrayToSize(_ size: Int) {
|
||||
let delta = size - audioSamples.count
|
||||
guard delta > 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
audioSamples = audioSamples + [Float](repeating: 0.0, count: delta)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess
|
||||
let playbackView: VoiceMessagePlaybackView
|
||||
|
||||
init() {
|
||||
playbackView = VoiceMessagePlaybackView.instanceFromNib()
|
||||
playbackView = VoiceMessagePlaybackView.loadFromNib()
|
||||
audioPlayer = VoiceMessageAudioPlayer()
|
||||
|
||||
timeFormatter = DateFormatter()
|
||||
@@ -52,6 +52,9 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess
|
||||
displayLink = CADisplayLink(target: WeakObjectWrapper(self), selector: #selector(handleDisplayLinkTick))
|
||||
displayLink.isPaused = true
|
||||
displayLink.add(to: .current, forMode: .common)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateTheme), name: .themeServiceDidChangeTheme, object: nil)
|
||||
updateTheme()
|
||||
}
|
||||
|
||||
var attachment: MXKAttachment? {
|
||||
@@ -198,4 +201,9 @@ class VoiceMessagePlaybackController: VoiceMessageAudioPlayerDelegate, VoiceMess
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@objc private func updateTheme() {
|
||||
playbackView.update(theme: ThemeService.shared().theme)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Reusable
|
||||
|
||||
protocol VoiceMessagePlaybackViewDelegate: AnyObject {
|
||||
func voiceMessagePlaybackViewDidRequestPlaybackToggle()
|
||||
@@ -29,9 +30,14 @@ struct VoiceMessagePlaybackViewDetails {
|
||||
var recording: Bool = false
|
||||
}
|
||||
|
||||
class VoiceMessagePlaybackView: UIView {
|
||||
class VoiceMessagePlaybackView: UIView, NibLoadable, Themable {
|
||||
|
||||
private enum Constants {
|
||||
static let backgroundCornerRadius: CGFloat = 12.0
|
||||
}
|
||||
|
||||
private var _waveformView: VoiceMessageWaveformView!
|
||||
private var currentTheme: Theme?
|
||||
|
||||
@IBOutlet private var backgroundView: UIView!
|
||||
@IBOutlet private var recordingIcon: UIView!
|
||||
@@ -47,20 +53,11 @@ class VoiceMessagePlaybackView: UIView {
|
||||
return _waveformView
|
||||
}
|
||||
|
||||
static func instanceFromNib() -> VoiceMessagePlaybackView {
|
||||
let nib = UINib(nibName: "VoiceMessagePlaybackView", 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()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleThemeDidChange), name: .themeServiceDidChangeTheme, object: nil)
|
||||
|
||||
backgroundView.layer.cornerRadius = 12.0
|
||||
backgroundView.layer.cornerRadius = Constants.backgroundCornerRadius
|
||||
playButton.layer.cornerRadius = playButton.bounds.width / 2.0
|
||||
|
||||
_waveformView = VoiceMessageWaveformView(frame: waveformContainerView.bounds)
|
||||
waveformContainerView.vc_addSubViewMatchingParent(_waveformView)
|
||||
@@ -72,6 +69,7 @@ class VoiceMessagePlaybackView: UIView {
|
||||
}
|
||||
|
||||
playButton.isEnabled = details.playbackEnabled
|
||||
playButton.setImage((details.playing ? Asset.Images.voiceMessagePauseButton.image : Asset.Images.voiceMessagePlayButton.image), for: .normal)
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
// UIStackView doesn't respond well to re-setting hidden states https://openradar.appspot.com/22819594
|
||||
@@ -88,23 +86,19 @@ class VoiceMessagePlaybackView: UIView {
|
||||
elapsedTimeLabel.text = details.currentTime
|
||||
_waveformView.progress = details.progress
|
||||
|
||||
if ThemeService.shared().isCurrentThemeDark() {
|
||||
playButton.setImage((details.playing ? Asset.Images.voiceMessagePauseButtonDark.image : Asset.Images.voiceMessagePlayButtonDark.image), for: .normal)
|
||||
backgroundView.backgroundColor = UIColor(rgb: 0x394049)
|
||||
_waveformView.primarylineColor = ThemeService.shared().theme.colors.quarterlyContent
|
||||
_waveformView.secondaryLineColor = ThemeService.shared().theme.colors.secondaryContent
|
||||
elapsedTimeLabel.textColor = UIColor(rgb: 0x8E99A4)
|
||||
} else {
|
||||
playButton.setImage((details.playing ? Asset.Images.voiceMessagePauseButtonLight.image : Asset.Images.voiceMessagePlayButtonLight.image), for: .normal)
|
||||
backgroundView.backgroundColor = UIColor(rgb: 0xE3E8F0)
|
||||
_waveformView.primarylineColor = ThemeService.shared().theme.colors.quarterlyContent
|
||||
_waveformView.secondaryLineColor = ThemeService.shared().theme.colors.secondaryContent
|
||||
elapsedTimeLabel.textColor = UIColor(rgb: 0x737D8C)
|
||||
}
|
||||
|
||||
_waveformView.setSamples(details.samples)
|
||||
|
||||
self.details = details
|
||||
|
||||
guard let theme = currentTheme else {
|
||||
return
|
||||
}
|
||||
|
||||
playButton.backgroundColor = theme.colors.separator
|
||||
backgroundView.backgroundColor = theme.colors.tile
|
||||
_waveformView.primarylineColor = theme.colors.quarterlyContent
|
||||
_waveformView.secondaryLineColor = theme.colors.secondaryContent
|
||||
elapsedTimeLabel.textColor = theme.colors.tertiaryContent
|
||||
}
|
||||
|
||||
func getRequiredNumberOfSamples() -> Int {
|
||||
@@ -113,13 +107,16 @@ class VoiceMessagePlaybackView: UIView {
|
||||
return _waveformView.requiredNumberOfSamples
|
||||
}
|
||||
|
||||
// MARK: - Themable
|
||||
|
||||
func update(theme: Theme) {
|
||||
currentTheme = theme
|
||||
configureWithDetails(details)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@IBAction private func onPlayButtonTap() {
|
||||
delegate?.voiceMessagePlaybackViewDidRequestPlaybackToggle()
|
||||
}
|
||||
|
||||
@objc private func handleThemeDidChange() {
|
||||
configureWithDetails(details)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,11 @@
|
||||
</imageView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GL1-b8-dZK">
|
||||
<rect key="frame" x="14" y="6" width="32" height="32"/>
|
||||
<state key="normal" image="voice_message_play_button_light"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="32" id="5Pl-ej-HIg"/>
|
||||
<constraint firstAttribute="width" constant="32" id="dXM-KA-xzM"/>
|
||||
</constraints>
|
||||
<state key="normal" image="voice_message_play_button"/>
|
||||
<connections>
|
||||
<action selector="onPlayButtonTap" destination="cGR-49-HWB" eventType="touchUpInside" id="B5j-st-pUp"/>
|
||||
</connections>
|
||||
@@ -79,7 +83,7 @@
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="voice_message_play_button_light" width="32" height="32"/>
|
||||
<image name="voice_message_play_button" width="12.5" height="15"/>
|
||||
<image name="voice_message_record_icon" width="10" height="10"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Reusable
|
||||
|
||||
protocol VoiceMessageToolbarViewDelegate: AnyObject {
|
||||
func voiceMessageToolbarViewDidRequestRecordingStart(_ toolbarView: VoiceMessageToolbarView)
|
||||
@@ -40,7 +41,15 @@ struct VoiceMessageToolbarViewDetails {
|
||||
var progress: Double = 0.0
|
||||
}
|
||||
|
||||
class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDelegate, VoiceMessagePlaybackViewDelegate {
|
||||
class VoiceMessageToolbarView: PassthroughView, NibLoadable, Themable, UIGestureRecognizerDelegate, VoiceMessagePlaybackViewDelegate {
|
||||
|
||||
private enum Constants {
|
||||
static let longPressMinimumDuration: TimeInterval = 0.1
|
||||
static let animationDuration: TimeInterval = 0.25
|
||||
static let lockModeTransitionAnimationDuration: TimeInterval = 0.5
|
||||
static let panDirectionChangeThreshold: CGFloat = 20.0
|
||||
}
|
||||
|
||||
@IBOutlet private var backgroundView: UIView!
|
||||
|
||||
@IBOutlet private var recordingContainerView: UIView!
|
||||
@@ -61,6 +70,8 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
|
||||
|
||||
@IBOutlet private var lockContainerView: UIView!
|
||||
@IBOutlet private var lockContainerBackgroundView: UIView!
|
||||
|
||||
@IBOutlet private var lockButtonsContainerView: UIView!
|
||||
@IBOutlet private var primaryLockButton: UIButton!
|
||||
@IBOutlet private var secondaryLockButton: UIButton!
|
||||
@IBOutlet private var lockChevron: UIView!
|
||||
@@ -86,31 +97,23 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
|
||||
}
|
||||
|
||||
weak var delegate: VoiceMessageToolbarViewDelegate?
|
||||
|
||||
@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)
|
||||
lockContainerBackgroundView.layer.cornerRadius = lockContainerBackgroundView.bounds.width / 2.0
|
||||
lockButtonsContainerView.layer.cornerRadius = lockButtonsContainerView.bounds.width / 2.0
|
||||
|
||||
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
|
||||
longPressGesture.delegate = self
|
||||
longPressGesture.minimumPressDuration = 0.1
|
||||
longPressGesture.minimumPressDuration = Constants.longPressMinimumDuration
|
||||
recordButtonsContainerView.addGestureRecognizer(longPressGesture)
|
||||
|
||||
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
|
||||
longPressGesture.delegate = self
|
||||
recordButtonsContainerView.addGestureRecognizer(panGesture)
|
||||
|
||||
playbackView = VoiceMessagePlaybackView.instanceFromNib()
|
||||
playbackView = VoiceMessagePlaybackView.loadFromNib()
|
||||
playbackView.delegate = self
|
||||
playbackViewContainerView.vc_addSubViewMatchingParent(playbackView)
|
||||
|
||||
@@ -123,7 +126,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
|
||||
func configureWithDetails(_ details: VoiceMessageToolbarViewDetails) {
|
||||
elapsedTimeLabel.text = details.elapsedTime
|
||||
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
UIView.animate(withDuration: Constants.animationDuration) {
|
||||
self.updatePlaybackViewWithDetails(details)
|
||||
}
|
||||
|
||||
@@ -136,7 +139,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
|
||||
convertedFrame = self.convert(lockChevron.frame, from: lockContainerView)
|
||||
lockChevronToRecordButtonDistance = recordButtonsContainerView.frame.midY + convertedFrame.maxY
|
||||
|
||||
lockChevronToLockButtonDistance = lockChevron.frame.minY - primaryLockButton.frame.midY
|
||||
lockChevronToLockButtonDistance = lockChevron.frame.minY - lockButtonsContainerView.frame.midY
|
||||
|
||||
startAnimatingRecordingIndicator()
|
||||
default:
|
||||
@@ -144,9 +147,8 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
|
||||
}
|
||||
|
||||
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
|
||||
UIView.animate(withDuration: Constants.animationDuration) {
|
||||
self.lockButtonsContainerView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
|
||||
} completion: { _ in
|
||||
self.updateUIWithDetails(details, animated: true)
|
||||
}
|
||||
@@ -166,6 +168,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
|
||||
|
||||
func update(theme: Theme) {
|
||||
currentTheme = theme
|
||||
playbackView.update(theme: theme)
|
||||
}
|
||||
|
||||
// MARK: - UIGestureRecognizerDelegate
|
||||
@@ -200,7 +203,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
|
||||
|
||||
let translation = gestureRecognizer.translation(in: self)
|
||||
|
||||
if abs(translation.x) <= 20.0 && abs(translation.y) <= 20.0 {
|
||||
if abs(translation.x) <= Constants.panDirectionChangeThreshold && abs(translation.y) <= Constants.panDirectionChangeThreshold {
|
||||
panDirection = nil
|
||||
} else if panDirection == nil {
|
||||
if abs(translation.x) >= abs(translation.y) {
|
||||
@@ -252,7 +255,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: (animated ? 0.25 : 0.0), delay: 0.0, options: .beginFromCurrentState) {
|
||||
UIView.animate(withDuration: (animated ? Constants.animationDuration : 0.0), delay: 0.0, options: .beginFromCurrentState) {
|
||||
switch details.state {
|
||||
case .record:
|
||||
self.backgroundView.alpha = 1.0
|
||||
@@ -297,20 +300,24 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
|
||||
return
|
||||
}
|
||||
|
||||
self.backgroundView.backgroundColor = theme.backgroundColor
|
||||
self.slideToCancelGradient.tintColor = theme.backgroundColor
|
||||
self.backgroundView.backgroundColor = theme.colors.background
|
||||
self.slideToCancelGradient.tintColor = theme.colors.background
|
||||
|
||||
self.primaryRecordButton.tintColor = theme.colors.tertiaryContent
|
||||
self.slideToCancelLabel.textColor = theme.colors.secondaryContent
|
||||
self.slideToCancelChevron.tintColor = theme.colors.secondaryContent
|
||||
self.elapsedTimeLabel.textColor = theme.colors.secondaryContent
|
||||
|
||||
self.lockContainerBackgroundView.backgroundColor = theme.colors.navigation
|
||||
self.lockButtonsContainerView.backgroundColor = theme.colors.navigation
|
||||
|
||||
self.primaryRecordButton.tintColor = theme.textTertiaryColor
|
||||
self.slideToCancelLabel.textColor = theme.textSecondaryColor
|
||||
self.slideToCancelChevron.tintColor = theme.textSecondaryColor
|
||||
self.elapsedTimeLabel.textColor = theme.textSecondaryColor
|
||||
} completion: { _ in
|
||||
switch details.state {
|
||||
case .idle:
|
||||
self.secondaryRecordButton.transform = .identity
|
||||
self.slideToCancelContainerView.transform = .identity
|
||||
self.lockChevron.transform = .identity
|
||||
self.secondaryLockButton.transform = .identity
|
||||
self.lockButtonsContainerView.transform = .identity
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -333,7 +340,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.5) {
|
||||
UIView.animate(withDuration: Constants.lockModeTransitionAnimationDuration) {
|
||||
if self.recordingIndicatorView.alpha > 0.0 {
|
||||
self.recordingIndicatorView.alpha = 0.0
|
||||
} else {
|
||||
@@ -341,8 +348,7 @@ class VoiceMessageToolbarView: PassthroughView, Themable, UIGestureRecognizerDel
|
||||
}
|
||||
} completion: { [weak self] _ in
|
||||
self?.startAnimatingRecordingIndicator()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction private func onTrashButtonTap(_ sender: UIBarItem) {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<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="XRB-CY-ijK">
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="XRB-CY-ijK" customClass="PassthroughView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="544" height="72"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8fP-9K-WTa">
|
||||
@@ -30,30 +30,45 @@
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.95686274510000002" green="0.97647058819999999" blue="0.99215686270000003" alpha="1" colorSpace="custom" customColorSpace="calibratedRGB"/>
|
||||
</view>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="YF2-5s-q5S">
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="C9P-A3-Vew">
|
||||
<rect key="frame" x="0.0" y="0.0" width="44" height="44"/>
|
||||
<state key="normal" image="voice_message_lock_icon_unlocked"/>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vm7-e1-VJ8">
|
||||
<rect key="frame" x="0.0" y="0.0" width="44" height="44"/>
|
||||
<state key="normal" image="voice_message_lock_icon_locked"/>
|
||||
</button>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="YF2-5s-q5S">
|
||||
<rect key="frame" x="0.0" y="0.0" width="44" height="44"/>
|
||||
<state key="normal" image="voice_message_lock_icon_unlocked"/>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vm7-e1-VJ8">
|
||||
<rect key="frame" x="0.0" y="0.0" width="44" height="44"/>
|
||||
<state key="normal" image="voice_message_lock_icon_locked"/>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="YF2-5s-q5S" secondAttribute="trailing" id="3a5-Dn-gjn"/>
|
||||
<constraint firstAttribute="bottom" secondItem="vm7-e1-VJ8" secondAttribute="bottom" id="87x-rV-hNr"/>
|
||||
<constraint firstItem="vm7-e1-VJ8" firstAttribute="leading" secondItem="C9P-A3-Vew" secondAttribute="leading" id="BeS-hI-uDa"/>
|
||||
<constraint firstAttribute="width" secondItem="C9P-A3-Vew" secondAttribute="height" multiplier="1:1" id="fuO-oh-g8I"/>
|
||||
<constraint firstAttribute="bottom" secondItem="YF2-5s-q5S" secondAttribute="bottom" id="rMf-if-5c3"/>
|
||||
<constraint firstItem="YF2-5s-q5S" firstAttribute="leading" secondItem="C9P-A3-Vew" secondAttribute="leading" id="rZq-pO-0OY"/>
|
||||
<constraint firstAttribute="trailing" secondItem="vm7-e1-VJ8" secondAttribute="trailing" id="rwd-uO-nu6"/>
|
||||
<constraint firstItem="YF2-5s-q5S" firstAttribute="top" secondItem="C9P-A3-Vew" secondAttribute="top" id="xJC-WF-SKy"/>
|
||||
<constraint firstItem="vm7-e1-VJ8" firstAttribute="top" secondItem="C9P-A3-Vew" secondAttribute="top" id="xzW-aL-alz"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="voice_message_lock_chevron" translatesAutoresizingMaskIntoConstraints="NO" id="c8y-xb-2nh">
|
||||
<rect key="frame" x="0.0" y="64" width="44" height="24"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YF2-5s-q5S" firstAttribute="top" secondItem="8fP-9K-WTa" secondAttribute="top" id="62w-uz-1IT"/>
|
||||
<constraint firstItem="YF2-5s-q5S" firstAttribute="leading" secondItem="8fP-9K-WTa" secondAttribute="leading" id="6sU-56-2eT"/>
|
||||
<constraint firstAttribute="trailing" secondItem="c8y-xb-2nh" secondAttribute="trailing" id="7HA-jr-fUD"/>
|
||||
<constraint firstItem="C9P-A3-Vew" firstAttribute="top" secondItem="8fP-9K-WTa" secondAttribute="top" id="7qH-6G-oAq"/>
|
||||
<constraint firstItem="c8y-xb-2nh" firstAttribute="centerY" secondItem="8fP-9K-WTa" secondAttribute="centerY" id="9x0-mO-M0V"/>
|
||||
<constraint firstAttribute="trailing" secondItem="C9P-A3-Vew" secondAttribute="trailing" id="DJV-ib-qiR"/>
|
||||
<constraint firstItem="C9P-A3-Vew" firstAttribute="leading" secondItem="8fP-9K-WTa" secondAttribute="leading" id="F2q-RQ-dgi"/>
|
||||
<constraint firstItem="c8y-xb-2nh" firstAttribute="leading" secondItem="8fP-9K-WTa" secondAttribute="leading" id="U4g-Vq-hJB"/>
|
||||
<constraint firstAttribute="width" constant="44" id="iwn-h5-ilH"/>
|
||||
<constraint firstAttribute="height" constant="152" id="li1-Bd-px2"/>
|
||||
<constraint firstItem="vm7-e1-VJ8" firstAttribute="centerY" secondItem="YF2-5s-q5S" secondAttribute="centerY" id="voJ-BE-HrY"/>
|
||||
<constraint firstItem="vm7-e1-VJ8" firstAttribute="centerX" secondItem="YF2-5s-q5S" secondAttribute="centerX" id="xKW-V1-MVd"/>
|
||||
<constraint firstAttribute="trailing" secondItem="YF2-5s-q5S" secondAttribute="trailing" id="zLG-ji-Sqy"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dyu-ha-046" customClass="PassthroughView" customModule="Riot" customModuleProvider="target">
|
||||
@@ -75,7 +90,7 @@
|
||||
</stackView>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" 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="217.5" height="72"/>
|
||||
<color key="tintColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</imageView>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="K6L-me-5EJ">
|
||||
<rect key="frame" x="20" y="11" width="64" height="50"/>
|
||||
@@ -211,6 +226,7 @@
|
||||
<outlet property="backgroundView" destination="FqE-3x-NQ9" id="RFR-SQ-s21"/>
|
||||
<outlet property="deleteButton" destination="U4V-EC-Ffy" id="Op3-oN-2vG"/>
|
||||
<outlet property="elapsedTimeLabel" destination="QBp-TZ-h5s" id="qC9-BQ-8RA"/>
|
||||
<outlet property="lockButtonsContainerView" destination="C9P-A3-Vew" id="ebu-OR-VXw"/>
|
||||
<outlet property="lockChevron" destination="c8y-xb-2nh" id="p6S-mB-C1U"/>
|
||||
<outlet property="lockContainerBackgroundView" destination="kvc-OZ-peC" id="ke4-gM-LQV"/>
|
||||
<outlet property="lockContainerView" destination="8fP-9K-WTa" id="mFH-Va-74i"/>
|
||||
@@ -239,8 +255,8 @@
|
||||
<image name="send_icon" width="36" height="36"/>
|
||||
<image name="voice_message_cancel_gradient" width="104" height="47"/>
|
||||
<image name="voice_message_lock_chevron" width="24" height="24"/>
|
||||
<image name="voice_message_lock_icon_locked" width="44" height="44"/>
|
||||
<image name="voice_message_lock_icon_unlocked" width="44" height="44"/>
|
||||
<image name="voice_message_lock_icon_locked" width="24" height="24"/>
|
||||
<image name="voice_message_lock_icon_unlocked" width="16" height="16"/>
|
||||
<image name="voice_message_record_button_default" width="22" height="26.5"/>
|
||||
<image name="voice_message_record_button_recording" width="52" height="52"/>
|
||||
<image name="voice_message_record_icon" width="10" height="10"/>
|
||||
|
||||
Reference in New Issue
Block a user