diff --git a/CHANGES.rst b/CHANGES.rst index bf77aedef..34b40353d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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) ================================================= diff --git a/Config/AppIdentifiers.xcconfig b/Config/AppIdentifiers.xcconfig index 1628e83d8..c21273565 100644 --- a/Config/AppIdentifiers.xcconfig +++ b/Config/AppIdentifiers.xcconfig @@ -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 diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording.png index 8fa147c18..5972e1272 100644 Binary files a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording.png and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@2x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@2x.png index f00a46204..802268ba0 100644 Binary files a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@2x.png and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@3x.png b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@3x.png index 7fdf91c21..b1def35e1 100644 Binary files a/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@3x.png and b/Riot/Assets/Images.xcassets/Room/VoiceMessages/voice_message_record_button_recording.imageset/voice_message_record_button_recording@3x.png differ diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 1349ade0d..1d3a38634 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -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 diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 3e991b86a..fa8d1a78a 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -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 diff --git a/Riot/Assets/it.lproj/InfoPlist.strings b/Riot/Assets/it.lproj/InfoPlist.strings index d46035a77..8e49c5075 100644 --- a/Riot/Assets/it.lproj/InfoPlist.strings +++ b/Riot/Assets/it.lproj/InfoPlist.strings @@ -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."; diff --git a/Riot/Assets/it.lproj/Vector.strings b/Riot/Assets/it.lproj/Vector.strings index 0a7d64e0e..51cb9ed06 100644 --- a/Riot/Assets/it.lproj/Vector.strings +++ b/Riot/Assets/it.lproj/Vector.strings @@ -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"; diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index 626dbaa96..b8eb3fc94 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -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 diff --git a/Riot/Assets/zh_Hans.lproj/Vector.strings b/Riot/Assets/zh_Hans.lproj/Vector.strings index 38f1729f1..e8b39a3c8 100644 --- a/Riot/Assets/zh_Hans.lproj/Vector.strings +++ b/Riot/Assets/zh_Hans.lproj/Vector.strings @@ -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 diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 73d4df484..f587e0b2d 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -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)") } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift index f6dc56577..40460f990 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageController.swift @@ -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) diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib index f11674470..52dcce870 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageToolbarView.xib @@ -23,7 +23,7 @@ - + @@ -120,15 +120,15 @@ - + @@ -136,14 +136,14 @@ - + - + @@ -152,7 +152,7 @@ - + @@ -280,7 +280,7 @@ - +