From 341b9bd6b84bf374646fe6354b8abae4f9a76d4d Mon Sep 17 00:00:00 2001 From: Philippe Loriaux Date: Wed, 23 Nov 2022 08:43:22 +0100 Subject: [PATCH] Add Voice Broadcast left time countdown --- Config/BuildSettings.swift | 2 +- .../Contents.json | 12 ++++++ .../voice_broadcast_time_left.svg | 3 ++ Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Images.swift | 1 + Riot/Generated/Strings.swift | 4 ++ .../VoiceBroadcastRecorderService.swift | 40 ++++++++++++++++++- ...oiceBroadcastRecorderServiceProtocol.swift | 1 + .../View/VoiceBroadcastRecorderView.swift | 10 +++++ .../VoiceBroadcastRecorderModels.swift | 6 +++ .../VoiceBroadcastRecorderScreenState.swift | 3 +- .../VoiceBroadcastRecorderViewModel.swift | 15 +++++++ 12 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/voice_broadcast_time_left.svg diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index d6b022948b..2f85f3c131 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -409,7 +409,7 @@ final class BuildSettings: NSObject { // MARK: - Voice Broadcast static let voiceBroadcastChunkLength: Int = 120 - static let voiceBroadcastMaxLength: UInt64 = 144000 + static let voiceBroadcastMaxLength: UInt = 14400 // 240min. // MARK: - MXKAppSettings static let enableBotCreation: Bool = false diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/Contents.json b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/Contents.json new file mode 100644 index 0000000000..6dbed56480 --- /dev/null +++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "voice_broadcast_time_left.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/voice_broadcast_time_left.svg b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/voice_broadcast_time_left.svg new file mode 100644 index 0000000000..eece236c88 --- /dev/null +++ b/Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/voice_broadcast_time_left.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index fa68764c3e..a39163b86f 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2196,6 +2196,7 @@ Tap the + to start adding people."; "voice_broadcast_playback_loading_error" = "Unable to play this voice broadcast."; "voice_broadcast_live" = "Live"; "voice_broadcast_tile" = "Voice broadcast"; +"voice_broadcast_time_left" = "%@ left"; // Mark: - Version check diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 94352b0be0..dcf78a2e13 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -347,6 +347,7 @@ internal class Asset: NSObject { internal static let voiceBroadcastStop = ImageAsset(name: "voice_broadcast_stop") internal static let voiceBroadcastTileLive = ImageAsset(name: "voice_broadcast_tile_live") internal static let voiceBroadcastTileMic = ImageAsset(name: "voice_broadcast_tile_mic") + internal static let voiceBroadcastTimeLeft = ImageAsset(name: "voice_broadcast_time_left") internal static let launchScreenLogo = ImageAsset(name: "launch_screen_logo") } @objcMembers diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 36091cc12f..78729dbf8c 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -9139,6 +9139,10 @@ public class VectorL10n: NSObject { public static var voiceBroadcastTile: String { return VectorL10n.tr("Vector", "voice_broadcast_tile") } + /// %@ left + public static func voiceBroadcastTimeLeft(_ p1: String) -> String { + return VectorL10n.tr("Vector", "voice_broadcast_time_left", p1) + } /// Can't start a new voice broadcast public static var voiceBroadcastUnauthorizedTitle: String { return VectorL10n.tr("Vector", "voice_broadcast_unauthorized_title") diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift index f2e28e5da9..10538095d2 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/MatrixSDK/VoiceBroadcastRecorderService.swift @@ -34,7 +34,13 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { private var chunkFile: AVAudioFile! = nil private var chunkFrames: AVAudioFrameCount = 0 private var chunkFileNumber: Int = 0 - + + private var currentElapsedTime: UInt = 0 // Time in seconds. + private var currentRemainingTime: UInt { // Time in seconds. + BuildSettings.voiceBroadcastMaxLength - currentElapsedTime + } + private var elapsedTimeTimer: Timer? + // MARK: Public weak var serviceDelegate: VoiceBroadcastRecorderServiceDelegate? @@ -67,12 +73,14 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { } try audioEngine.start() + startTimer() // Disable the sleep mode during the recording until we are able to handle it UIApplication.shared.isIdleTimerDisabled = true } catch { MXLog.debug("[VoiceBroadcastRecorderService] startRecordingVoiceBroadcast error", context: error) stopRecordingVoiceBroadcast() + invalidateTimer() } } @@ -81,6 +89,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { audioEngine.stop() audioEngine.inputNode.removeTap(onBus: audioNodeBus) UIApplication.shared.isIdleTimerDisabled = false + invalidateTimer() voiceBroadcastService?.stopVoiceBroadcast(success: { [weak self] _ in MXLog.debug("[VoiceBroadcastRecorderService] Stopped") @@ -110,6 +119,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { func pauseRecordingVoiceBroadcast() { audioEngine.pause() UIApplication.shared.isIdleTimerDisabled = false + invalidateTimer() voiceBroadcastService?.pauseVoiceBroadcast(success: { [weak self] _ in guard let self = self else { return } @@ -126,6 +136,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { func resumeRecordingVoiceBroadcast() { try? audioEngine.start() + startTimer() voiceBroadcastService?.resumeVoiceBroadcast(success: { [weak self] _ in guard let self = self else { return } @@ -143,12 +154,14 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { private func resetValues() { chunkFrames = 0 chunkFileNumber = 0 + currentElapsedTime = 0 } /// Release the service private func tearDownVoiceBroadcastService() { resetValues() session.tearDownVoiceBroadcastService() + invalidateTimer() do { try AVAudioSession.sharedInstance().setActive(false) @@ -157,6 +170,31 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol { } } + /// Start ElapsedTimeTimer. + private func startTimer() { + elapsedTimeTimer = Timer.scheduledTimer(timeInterval: 1.0, + target: self, + selector: #selector(updateCurrentElapsedTimeValue), + userInfo: nil, + repeats: true) + } + + /// Invalidate ElapsedTimeTimer. + private func invalidateTimer() { + elapsedTimeTimer?.invalidate() + elapsedTimeTimer = nil + } + + /// Update currentElapsedTime value. + @objc private func updateCurrentElapsedTimeValue() { + guard currentRemainingTime > 0 else { + stopRecordingVoiceBroadcast() + return + } + currentElapsedTime += 1 + serviceDelegate?.voiceBroadcastRecorderService(self, didUpdateRemainingTime: self.currentRemainingTime) + } + /// Write audio buffer to chunk file. private func writeBuffer(_ buffer: AVAudioPCMBuffer) { let sampleRate = buffer.format.sampleRate diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift index 7b97eb83a2..e457eb843c 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/Service/VoiceBroadcastRecorderServiceProtocol.swift @@ -18,6 +18,7 @@ import Foundation protocol VoiceBroadcastRecorderServiceDelegate: AnyObject { func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateState state: VoiceBroadcastRecorderState) + func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateRemainingTime remainingTime: UInt) } protocol VoiceBroadcastRecorderServiceProtocol { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift index 411ce0333b..8a5de33aa2 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift @@ -53,6 +53,16 @@ struct VoiceBroadcastRecorderView: View { } icon: { Image(uiImage: Asset.Images.voiceBroadcastTileLive.image) } + + if let remainingTimeLabel = viewModel.viewState.currentRecordingState.remainingTimeLabel { + Label { + Text(remainingTimeLabel) + .foregroundColor(theme.colors.secondaryContent) + .font(theme.fonts.caption1) + } icon: { + Image(uiImage: Asset.Images.voiceBroadcastTimeLeft.image) + } + } }.frame(maxWidth: .infinity, alignment: .leading) Label { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift index 7a2566aad7..ebc1f39c31 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderModels.swift @@ -35,9 +35,15 @@ struct VoiceBroadcastRecorderDetails { let avatarData: AvatarInputProtocol } +struct VoiceBroadcastRecordingState { + var remainingTime: UInt + var remainingTimeLabel: String? +} + struct VoiceBroadcastRecorderViewState: BindableState { var details: VoiceBroadcastRecorderDetails var recordingState: VoiceBroadcastRecorderState + var currentRecordingState: VoiceBroadcastRecordingState var bindings: VoiceBroadcastRecorderViewStateBindings } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift index bc915d36a3..8962da13d2 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderScreenState.swift @@ -32,7 +32,8 @@ enum MockVoiceBroadcastRecorderScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { let details = VoiceBroadcastRecorderDetails(senderDisplayName: "", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room")) - let viewModel = MockVoiceBroadcastRecorderViewModel(initialViewState: VoiceBroadcastRecorderViewState(details: details, recordingState: .started, bindings: VoiceBroadcastRecorderViewStateBindings())) + let recordingState = VoiceBroadcastRecordingState(remainingTime: BuildSettings.voiceBroadcastMaxLength) + let viewModel = MockVoiceBroadcastRecorderViewModel(initialViewState: VoiceBroadcastRecorderViewState(details: details, recordingState: .started, currentRecordingState: recordingState, bindings: VoiceBroadcastRecorderViewStateBindings())) return ( [false, viewModel], diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift index 6e14441620..27bd2f6e83 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/VoiceBroadcastRecorderViewModel.swift @@ -34,8 +34,10 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic init(details: VoiceBroadcastRecorderDetails, recorderService: VoiceBroadcastRecorderServiceProtocol) { self.voiceBroadcastRecorderService = recorderService + let currentRecordingState = VoiceBroadcastRecordingState(remainingTime: BuildSettings.voiceBroadcastMaxLength) super.init(initialViewState: VoiceBroadcastRecorderViewState(details: details, recordingState: .stopped, + currentRecordingState: currentRecordingState, bindings: VoiceBroadcastRecorderViewStateBindings())) self.voiceBroadcastRecorderService.serviceDelegate = self @@ -77,10 +79,23 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic self.state.recordingState = .resumed voiceBroadcastRecorderService.resumeRecordingVoiceBroadcast() } + + private func updateRemainingTime(_ remainingTime: UInt) { + let time = TimeInterval(Double(remainingTime)) + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .abbreviated + + state.currentRecordingState.remainingTime = remainingTime + state.currentRecordingState.remainingTimeLabel = VectorL10n.voiceBroadcastTimeLeft(formatter.string(from: time) ?? "0s") + } } extension VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderServiceDelegate { func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateState state: VoiceBroadcastRecorderState) { self.state.recordingState = state } + + func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateRemainingTime remainingTime: UInt) { + self.updateRemainingTime(remainingTime) + } }