mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-06 07:57:42 +02:00
Merge branch 'release/1.4.9/master'
This commit is contained in:
+28
@@ -1,3 +1,31 @@
|
||||
Changes in 1.4.9 (2021-08-03)
|
||||
=================================================
|
||||
|
||||
✨ Features
|
||||
*
|
||||
|
||||
🙌 Improvements
|
||||
* Voice Messages: Increased recording state microphone icon size
|
||||
* Voice Messages: Using "Voice message - MM.dd.yyyy HH.mm.ss" as the format for recorded audio files
|
||||
|
||||
🐛 Bugfix
|
||||
* Voice Messages: Fixed race conditions when sending voice messages (#4641)
|
||||
|
||||
⚠️ API Changes
|
||||
*
|
||||
|
||||
🗣 Translations
|
||||
*
|
||||
|
||||
🧱 Build
|
||||
*
|
||||
|
||||
Others
|
||||
*
|
||||
|
||||
Improvements:
|
||||
|
||||
|
||||
Changes in 1.4.8 (2021-07-29)
|
||||
=================================================
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ APPLICATION_GROUP_IDENTIFIER = group.im.vector
|
||||
APPLICATION_SCHEME = element
|
||||
|
||||
// Version
|
||||
MARKETING_VERSION = 1.4.8
|
||||
CURRENT_PROJECT_VERSION = 1.4.8
|
||||
MARKETING_VERSION = 1.4.9
|
||||
CURRENT_PROJECT_VERSION = 1.4.9
|
||||
|
||||
|
||||
// Team
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.8 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 5.4 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 8.1 KiB |
@@ -1361,7 +1361,7 @@
|
||||
// Room Notification Settings
|
||||
"room_notifs_settings_notify_me_for" = "Teavita mind";
|
||||
"room_details_notifs" = "Teavitused";
|
||||
"voice_message_stop_locked_mode_recording" = "Salvestuse peatamiseks ja taasesituseks vajuta lainekese nuppu";
|
||||
"voice_message_stop_locked_mode_recording" = "Salvestuse peatamiseks ja taasesituseks vajuta salvestuse vaadet";
|
||||
"voice_message_remaining_recording_time" = "salvestusaega jäänud %@s";
|
||||
|
||||
// Mark: - Voice Messages
|
||||
|
||||
@@ -1424,7 +1424,7 @@
|
||||
// Room Notification Settings
|
||||
"room_notifs_settings_notify_me_for" = "Értesítés ezért:";
|
||||
"room_details_notifs" = "Értesítések";
|
||||
"voice_message_stop_locked_mode_recording" = "Megállításhoz és visszajátszáshoz koppints a hullámhosszra";
|
||||
"voice_message_stop_locked_mode_recording" = "Megállításhoz és visszajátszáshoz koppints a felvételre";
|
||||
"voice_message_remaining_recording_time" = "%@s távozott";
|
||||
|
||||
// Mark: - Voice Messages
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Permissions usage explanations
|
||||
"NSCameraUsageDescription" = "La fotocamera viene utilizzata per scattare fotografie, registrare video ed eseguire videochiamate.";
|
||||
"NSPhotoLibraryUsageDescription" = "La libreria fotografica viene utilizzata per inviare foto e video.";
|
||||
"NSMicrophoneUsageDescription" = "Il microfono viene utilizzato per registrare video ed effettuare chiamate.";
|
||||
"NSMicrophoneUsageDescription" = "Element ha bisogno di accedere al microfono per effettuare e ricevere chiamate, registrare video e messaggi vocali.";
|
||||
"NSContactsUsageDescription" = "Per scoprire i contatti che già usano Matrix, Element può inviare gli indirizzi email e i numeri di telefono della tua rubrica al server identità che hai scelto. Se supportato, viene fatto un hash dei dati personali prima dell'invio - controlla la politica sulla privacy del tuo server di identità per maggiori informazioni.";
|
||||
"NSCalendarsUsageDescription" = "Vedi le tue riunioni programmate nell'app.";
|
||||
"NSFaceIDUsageDescription" = "Face ID viene usato per accedere all'app.";
|
||||
|
||||
@@ -1395,3 +1395,10 @@
|
||||
// Room Notification Settings
|
||||
"room_notifs_settings_notify_me_for" = "Inviami notifiche per";
|
||||
"room_details_notifs" = "Notifiche";
|
||||
"voice_message_stop_locked_mode_recording" = "Tocca la registrazione per fermare o ascoltare";
|
||||
"voice_message_remaining_recording_time" = "%@s rimasti";
|
||||
|
||||
// Mark: - Voice Messages
|
||||
|
||||
"voice_message_release_to_send" = "Tieni premuto per registrare, rilascia per inviare";
|
||||
"settings_labs_voice_messages" = "Messaggi vocali";
|
||||
|
||||
@@ -1393,7 +1393,7 @@
|
||||
"room_notifs_settings_notify_me_for" = "Notifique-me para";
|
||||
"room_details_notifs" = "Notificações";
|
||||
"voice_message_remaining_recording_time" = "%@s restando";
|
||||
"voice_message_stop_locked_mode_recording" = "Toque no comprimento de onda para parar e dar playback";
|
||||
"voice_message_stop_locked_mode_recording" = "Toque em sua gravação para parar ou escutar";
|
||||
|
||||
// Mark: - Voice Messages
|
||||
|
||||
|
||||
@@ -1426,7 +1426,7 @@
|
||||
"settings_ui_theme_picker_message_invert_colours" = "“自动”使用您设备的“反转颜色”设置";
|
||||
"room_recents_unknown_room_error_message" = "找不到这个房间。 确保它存在";
|
||||
"room_creation_dm_error" = "我们无法创建您的 DM。 请检查您要邀请的用户,然后重试。";
|
||||
"voice_message_stop_locked_mode_recording" = "轻按波长停止和回放消息";
|
||||
"voice_message_stop_locked_mode_recording" = "轻按录音停止或收听";
|
||||
"voice_message_remaining_recording_time" = "剩 %@s";
|
||||
|
||||
// Mark: - Voice Messages
|
||||
|
||||
@@ -84,6 +84,7 @@ class VoiceMessageAttachmentCacheManager {
|
||||
workQueue.async {
|
||||
// Run this in the work queue to preserve order
|
||||
if let finalURL = self.finalURLs[identifier], let duration = self.durations[identifier], let samples = self.samples[identifier]?[numberOfSamples] {
|
||||
MXLog.debug("[VoiceMessageAttachmentCacheManager] Finished task - using cached results")
|
||||
let result = VoiceMessageAttachmentCacheManagerLoadResult(eventIdentifier: identifier, url: finalURL, duration: duration, samples: samples)
|
||||
DispatchQueue.main.async {
|
||||
completion(Result.success(result))
|
||||
@@ -109,22 +110,93 @@ class VoiceMessageAttachmentCacheManager {
|
||||
completionCallbacks[callbackKey] = [CompletionWrapper(completion)]
|
||||
}
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
if let finalURL = finalURLs[identifier], let duration = durations[identifier] {
|
||||
sampleFileAtURL(finalURL, duration: duration, numberOfSamples: numberOfSamples, identifier: identifier)
|
||||
return
|
||||
}
|
||||
|
||||
func sampleFileAtURL(_ url: URL, duration: TimeInterval) {
|
||||
let analyser = WaveformAnalyzer(audioAssetURL: url)
|
||||
|
||||
dispatchGroup.enter()
|
||||
analyser?.samples(count: numberOfSamples, completionHandler: { samples in
|
||||
MXLog.debug("[VoiceMessageAttachmentCacheManager] Finished sampling voice message")
|
||||
|
||||
dispatchGroup.leave()
|
||||
|
||||
DispatchQueue.main.async { // These don't behave accordingly if called from a background thread
|
||||
if attachment.isEncrypted {
|
||||
attachment.decrypt(toTempFile: { filePath in
|
||||
self.workQueue.async {
|
||||
self.convertFileAtPath(filePath, numberOfSamples: numberOfSamples, identifier: identifier)
|
||||
}
|
||||
}, failure: { error in
|
||||
// A nil error in this case is a cancellation on the MXMediaLoader
|
||||
if let error = error {
|
||||
MXLog.error("Failed decrypting attachment with error: \(String(describing: error))")
|
||||
self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.decryptionError(error))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
attachment.prepare({
|
||||
self.workQueue.async {
|
||||
self.convertFileAtPath(attachment.cacheFilePath, numberOfSamples: numberOfSamples, identifier: identifier)
|
||||
}
|
||||
}, failure: { error in
|
||||
// A nil error in this case is a cancellation on the MXMediaLoader
|
||||
if let error = error {
|
||||
MXLog.error("Failed preparing attachment with error: \(String(describing: error))")
|
||||
self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.preparationError(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func convertFileAtPath(_ path: String?, numberOfSamples: Int, identifier: String) {
|
||||
guard let filePath = path else {
|
||||
return
|
||||
}
|
||||
|
||||
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||
let newURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a")
|
||||
|
||||
VoiceMessageAudioConverter.convertToMPEG4AAC(sourceURL: URL(fileURLWithPath: filePath), destinationURL: newURL) { result in
|
||||
MXLog.debug("[VoiceMessageAttachmentCacheManager] Finished converting voice message")
|
||||
self.workQueue.async {
|
||||
switch result {
|
||||
case .success:
|
||||
self.finalURLs[identifier] = newURL
|
||||
|
||||
VoiceMessageAudioConverter.mediaDurationAt(newURL) { result in
|
||||
self.workQueue.async {
|
||||
MXLog.debug("[VoiceMessageAttachmentCacheManager] Finished retrieving media duration")
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
if let duration = try? result.get() {
|
||||
self.durations[identifier] = duration
|
||||
self.sampleFileAtURL(newURL, duration: duration, numberOfSamples: numberOfSamples, identifier: identifier)
|
||||
} else {
|
||||
MXLog.error("[VoiceMessageAttachmentCacheManager] Failed retrieving media duration")
|
||||
}
|
||||
case .failure(let error):
|
||||
MXLog.error("[VoiceMessageAttachmentCacheManager] Failed retrieving audio duration with error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
MXLog.error("[VoiceMessageAttachmentCacheManager] Failed decoding audio message with error: \(error)")
|
||||
self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.conversionError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sampleFileAtURL(_ url: URL, duration: TimeInterval, numberOfSamples: Int, identifier: String) {
|
||||
let analyser = WaveformAnalyzer(audioAssetURL: url)
|
||||
|
||||
analyser?.samples(count: numberOfSamples, completionHandler: { samples in
|
||||
self.workQueue.async {
|
||||
guard let samples = samples else {
|
||||
MXLog.debug("[VoiceMessageAttachmentCacheManager] Failed sampling voice message")
|
||||
self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.samplingError)
|
||||
return
|
||||
}
|
||||
|
||||
MXLog.debug("[VoiceMessageAttachmentCacheManager] Finished sampling voice message")
|
||||
|
||||
if var existingSamples = self.samples[identifier] {
|
||||
existingSamples[numberOfSamples] = samples
|
||||
self.samples[identifier] = existingSamples
|
||||
@@ -133,86 +205,8 @@ class VoiceMessageAttachmentCacheManager {
|
||||
}
|
||||
|
||||
self.invokeSuccessCallbacksForIdentifier(identifier, url: url, duration: duration, samples: samples)
|
||||
})
|
||||
}
|
||||
|
||||
if let finalURL = finalURLs[identifier], let duration = durations[identifier] {
|
||||
sampleFileAtURL(finalURL, duration: duration)
|
||||
dispatchGroup.wait()
|
||||
MXLog.debug("[VoiceMessageAttachmentCacheManager] Finished task")
|
||||
return
|
||||
}
|
||||
|
||||
func convertFileAtPath(_ path: String?) {
|
||||
guard let filePath = path else {
|
||||
return
|
||||
}
|
||||
|
||||
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||
let newURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a")
|
||||
|
||||
dispatchGroup.enter()
|
||||
VoiceMessageAudioConverter.convertToMPEG4AAC(sourceURL: URL(fileURLWithPath: filePath), destinationURL: newURL) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.finalURLs[identifier] = newURL
|
||||
VoiceMessageAudioConverter.mediaDurationAt(newURL) { result in
|
||||
MXLog.debug("[VoiceMessageAttachmentCacheManager] Finished converting voice message")
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
if let duration = try? result.get() {
|
||||
self.durations[identifier] = duration
|
||||
sampleFileAtURL(newURL, duration: duration)
|
||||
} else {
|
||||
MXLog.error("[VoiceMessageAttachmentCacheManager] enqueueLoadAttachment: Failed to retrieve media duration")
|
||||
}
|
||||
case .failure(let error):
|
||||
MXLog.error("[VoiceMessageAttachmentCacheManager] enqueueLoadAttachment: failed getting audio duration with: \(error)")
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
case .failure(let error):
|
||||
self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.conversionError(error))
|
||||
MXLog.error("[VoiceMessageAttachmentCacheManager] enqueueLoadAttachment: failed decoding audio message with: \(error)")
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.enter()
|
||||
DispatchQueue.main.async { // These don't behave accordingly if called from a background thread
|
||||
if attachment.isEncrypted {
|
||||
attachment.decrypt(toTempFile: { filePath in
|
||||
convertFileAtPath(filePath)
|
||||
dispatchGroup.leave()
|
||||
}, failure: { error in
|
||||
// A nil error in this case is a cancellation on the MXMediaLoader
|
||||
if let error = error {
|
||||
MXLog.error("Failed decrypting attachment with error: \(String(describing: error))")
|
||||
self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.decryptionError(error))
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
})
|
||||
} else {
|
||||
attachment.prepare({
|
||||
convertFileAtPath(attachment.cacheFilePath)
|
||||
dispatchGroup.leave()
|
||||
}, failure: { error in
|
||||
// A nil error in this case is a cancellation on the MXMediaLoader
|
||||
if let error = error {
|
||||
MXLog.error("Failed preparing attachment with error: \(String(describing: error))")
|
||||
self.invokeFailureCallbacksForIdentifier(identifier, error: VoiceMessageAttachmentCacheManagerError.preparationError(error))
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.wait()
|
||||
|
||||
MXLog.debug("[VoiceMessageAttachmentCacheManager] Finished task")
|
||||
})
|
||||
}
|
||||
|
||||
private func invokeSuccessCallbacksForIdentifier(_ identifier: String, url: URL, duration: TimeInterval, samples: [Float]) {
|
||||
@@ -232,6 +226,8 @@ class VoiceMessageAttachmentCacheManager {
|
||||
}
|
||||
|
||||
self.completionCallbacks[callbackKey] = nil
|
||||
|
||||
MXLog.debug("[VoiceMessageAttachmentCacheManager] Successfully finished task")
|
||||
}
|
||||
|
||||
private func invokeFailureCallbacksForIdentifier(_ identifier: String, error: Error) {
|
||||
@@ -249,5 +245,7 @@ class VoiceMessageAttachmentCacheManager {
|
||||
}
|
||||
|
||||
self.completionCallbacks[callbackKey] = nil
|
||||
|
||||
MXLog.debug("[VoiceMessageAttachmentCacheManager] Failed task with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,13 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
static let maximumAudioRecordingDuration: TimeInterval = 120.0
|
||||
static let maximumAudioRecordingLengthReachedThreshold: TimeInterval = 10.0
|
||||
static let elapsedTimeFormat = "m:ss"
|
||||
static let fileNameFormat = "'Voice message - 'MM.dd.yyyy HH.mm.ss"
|
||||
static let minimumRecordingDuration = 1.0
|
||||
}
|
||||
|
||||
private let themeService: ThemeService
|
||||
private let mediaServiceProvider: VoiceMessageMediaServiceProvider
|
||||
private let temporaryFileURL: URL
|
||||
private var temporaryFileURL: URL!
|
||||
|
||||
private let _voiceMessageToolbarView: VoiceMessageToolbarView
|
||||
private var displayLink: CADisplayLink!
|
||||
@@ -48,12 +49,18 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
private var isInLockedMode: Bool = false
|
||||
private var notifiedRemainingTime = false
|
||||
|
||||
private static let timeFormatter: DateFormatter = {
|
||||
private static let elapsedTimeFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = Constants.elapsedTimeFormat
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
private static let fileNameDateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = Constants.fileNameFormat
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
@objc public weak var delegate: VoiceMessageControllerDelegate?
|
||||
|
||||
@objc public var isRecordingAudio: Bool {
|
||||
@@ -68,9 +75,6 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
self.themeService = themeService
|
||||
self.mediaServiceProvider = mediaServiceProvider
|
||||
|
||||
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||
temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(ProcessInfo().globallyUniqueString).appendingPathExtension("m4a")
|
||||
|
||||
_voiceMessageToolbarView = VoiceMessageToolbarView.loadFromNib()
|
||||
|
||||
super.init()
|
||||
@@ -107,6 +111,11 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
|
||||
audioRecorder = mediaServiceProvider.audioRecorder()
|
||||
audioRecorder?.registerDelegate(self)
|
||||
|
||||
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||
let fileName = VoiceMessageController.fileNameDateFormatter.string(from: Date())
|
||||
temporaryFileURL = temporaryDirectoryURL.appendingPathComponent(fileName).appendingPathExtension("m4a")
|
||||
|
||||
audioRecorder?.recordWithOutputURL(temporaryFileURL)
|
||||
}
|
||||
|
||||
@@ -260,7 +269,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
})
|
||||
|
||||
dispatchGroup.enter()
|
||||
let destinationURL = sourceURL.deletingPathExtension().appendingPathExtension("opus")
|
||||
let destinationURL = sourceURL.deletingPathExtension().appendingPathExtension("ogg")
|
||||
VoiceMessageAudioConverter.convertToOpusOgg(sourceURL: sourceURL, destinationURL: destinationURL) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
@@ -342,7 +351,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
|
||||
var details = VoiceMessageToolbarViewDetails()
|
||||
details.state = (isRecording ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle))
|
||||
details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: currentTime))
|
||||
details.elapsedTime = VoiceMessageController.elapsedTimeFormatter.string(from: Date(timeIntervalSinceReferenceDate: currentTime))
|
||||
details.audioSamples = audioSamples
|
||||
|
||||
if isRecording {
|
||||
@@ -391,7 +400,7 @@ public class VoiceMessageController: NSObject, VoiceMessageToolbarViewDelegate,
|
||||
|
||||
var details = VoiceMessageToolbarViewDetails()
|
||||
details.state = (audioRecorder?.isRecording ?? false ? (isInLockedMode ? .lockedModeRecord : .record) : (isInLockedMode ? .lockedModePlayback : .idle))
|
||||
details.elapsedTime = VoiceMessageController.timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration)))
|
||||
details.elapsedTime = VoiceMessageController.elapsedTimeFormatter.string(from: Date(timeIntervalSinceReferenceDate: (audioPlayer.isPlaying ? audioPlayer.currentTime : audioPlayer.duration)))
|
||||
details.audioSamples = audioSamples
|
||||
details.isPlaying = audioPlayer.isPlaying
|
||||
details.progress = (audioPlayer.isPlaying ? (audioPlayer.duration > 0.0 ? audioPlayer.currentTime / audioPlayer.duration : 0.0) : 0.0)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="544" height="72"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="8fP-9K-WTa">
|
||||
<rect key="frame" x="492" y="-90" width="44" height="152"/>
|
||||
<rect key="frame" x="492" y="-80" width="44" height="152"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kvc-OZ-peC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="44" height="152"/>
|
||||
@@ -120,15 +120,15 @@
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7OQ-1F-5qT">
|
||||
<rect key="frame" x="488" y="10" width="52" height="52"/>
|
||||
<rect key="frame" x="478" y="0.0" width="72" height="72"/>
|
||||
<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"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="72" height="72"/>
|
||||
<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"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="72" height="72"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
|
||||
<state key="normal" image="voice_message_record_button_recording"/>
|
||||
</button>
|
||||
</subviews>
|
||||
@@ -136,14 +136,14 @@
|
||||
<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="height" constant="72" 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"/>
|
||||
<constraint firstAttribute="width" constant="72" id="zPb-1B-JyA"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
@@ -152,7 +152,7 @@
|
||||
<constraint firstItem="dyu-ha-046" firstAttribute="leading" secondItem="XRB-CY-ijK" secondAttribute="leading" id="BoC-Ut-chI"/>
|
||||
<constraint firstAttribute="bottom" secondItem="dyu-ha-046" secondAttribute="bottom" id="U4h-FY-D3W"/>
|
||||
<constraint firstItem="8fP-9K-WTa" firstAttribute="bottom" secondItem="7OQ-1F-5qT" secondAttribute="bottom" id="X4v-7T-LgP"/>
|
||||
<constraint firstAttribute="trailing" secondItem="7OQ-1F-5qT" secondAttribute="trailing" constant="4" id="giC-4J-EUL"/>
|
||||
<constraint firstAttribute="trailing" secondItem="7OQ-1F-5qT" secondAttribute="trailing" constant="-6" id="giC-4J-EUL"/>
|
||||
<constraint firstItem="dyu-ha-046" firstAttribute="top" secondItem="XRB-CY-ijK" secondAttribute="top" id="ra2-Me-23b"/>
|
||||
<constraint firstItem="8fP-9K-WTa" firstAttribute="centerX" secondItem="7OQ-1F-5qT" secondAttribute="centerX" id="xL5-g3-aHb"/>
|
||||
<constraint firstAttribute="trailing" secondItem="dyu-ha-046" secondAttribute="trailing" id="xME-WZ-OMX"/>
|
||||
@@ -280,7 +280,7 @@
|
||||
<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_button_recording" width="72" height="72"/>
|
||||
<image name="voice_message_record_icon" width="10" height="10"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
Reference in New Issue
Block a user