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..82b9eb425a --- /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 68a4f873d0..e9295cc9e2 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2202,6 +2202,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 41b878426c..f773146b2d 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -9155,6 +9155,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/Riot/Modules/Room/TimelineCells/SizableCell/SizableBaseRoomCell.swift b/Riot/Modules/Room/TimelineCells/SizableCell/SizableBaseRoomCell.swift index f33762144e..b8ba675b2c 100644 --- a/Riot/Modules/Room/TimelineCells/SizableCell/SizableBaseRoomCell.swift +++ b/Riot/Modules/Room/TimelineCells/SizableCell/SizableBaseRoomCell.swift @@ -65,6 +65,12 @@ class SizableBaseRoomCell: BaseRoomCell, SizableBaseRoomCellType { return self.height(for: roomBubbleCellData, fitting: maxWidth) } + + override func prepareForReuse() { + cleanContentVC() + + super.prepareForReuse() + } // MARK - SizableBaseRoomCellType @@ -173,10 +179,21 @@ class SizableBaseRoomCell: BaseRoomCell, SizableBaseRoomCellType { } return height - } - + } + + private func cleanContentVC() { + contentVC?.removeFromParent() + contentVC?.view.removeFromSuperview() + contentVC?.didMove(toParent: nil) + contentVC = nil + } + + // MARK: - Public + func addContentViewController(_ controller: UIViewController, on contentView: UIView) { controller.view.invalidateIntrinsicContentSize() + + cleanContentVC() let parent = vc_parentViewController parent?.addChild(controller) @@ -185,13 +202,4 @@ class SizableBaseRoomCell: BaseRoomCell, SizableBaseRoomCellType { contentVC = controller } - - override func prepareForReuse() { - contentVC?.removeFromParent() - contentVC?.view.removeFromSuperview() - contentVC?.didMove(toParent: nil) - contentVC = nil - - super.prepareForReuse() - } } 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..6c2c21e3cb 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastRecorder/View/VoiceBroadcastRecorderView.swift @@ -53,6 +53,14 @@ struct VoiceBroadcastRecorderView: View { } icon: { Image(uiImage: Asset.Images.voiceBroadcastTileLive.image) } + + Label { + Text(viewModel.viewState.currentRecordingState.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..cb807a430c 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..c2b57dc5ce 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, remainingTimeLabel: "1h 20m 47s left") + 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..ba9690bfb1 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 = VoiceBroadcastRecorderViewModel.currentRecordingState(from: BuildSettings.voiceBroadcastMaxLength) super.init(initialViewState: VoiceBroadcastRecorderViewState(details: details, recordingState: .stopped, + currentRecordingState: currentRecordingState, bindings: VoiceBroadcastRecorderViewStateBindings())) self.voiceBroadcastRecorderService.serviceDelegate = self @@ -77,10 +79,27 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic self.state.recordingState = .resumed voiceBroadcastRecorderService.resumeRecordingVoiceBroadcast() } + + private func updateRemainingTime(_ remainingTime: UInt) { + state.currentRecordingState = VoiceBroadcastRecorderViewModel.currentRecordingState(from: remainingTime) + } + + private static func currentRecordingState(from remainingTime: UInt) -> VoiceBroadcastRecordingState { + let time = TimeInterval(Double(remainingTime)) + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .abbreviated + + return VoiceBroadcastRecordingState(remainingTime: remainingTime, + 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) + } } diff --git a/changelog.d/pr-7103.feature b/changelog.d/pr-7103.feature new file mode 100644 index 0000000000..4ce05d7cb3 --- /dev/null +++ b/changelog.d/pr-7103.feature @@ -0,0 +1 @@ +Add the left time in the Voice Broadcast tile recorder. diff --git a/changelog.d/pr-7105.bugfix b/changelog.d/pr-7105.bugfix new file mode 100644 index 0000000000..c1125fcaa9 --- /dev/null +++ b/changelog.d/pr-7105.bugfix @@ -0,0 +1 @@ +Fix scroll issues with VoiceBroadcast and Poll cells