diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 49b6fb73c..e20ba1c41 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2225,6 +2225,7 @@ Tap the + to start adding people."; "voice_broadcast_connection_error_title" = "Connection error"; "voice_broadcast_connection_error_message" = "Unfortunately we’re unable to start a recording right now. Please try again later."; "voice_broadcast_recorder_connection_error" = "Connection error - Recording paused"; +"voice_broadcast_playback_unable_to_decrypt" = "Unable to decrypt this voice broadcast."; // MARK: - Version check diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 44ea603d0..02f908418 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -9235,6 +9235,10 @@ public class VectorL10n: NSObject { public static var voiceBroadcastPlaybackLockScreenPlaceholder: String { return VectorL10n.tr("Vector", "voice_broadcast_playback_lock_screen_placeholder") } + /// Unable to decrypt this voice broadcast. + public static var voiceBroadcastPlaybackUnableToDecrypt: String { + return VectorL10n.tr("Vector", "voice_broadcast_playback_unable_to_decrypt") + } /// Connection error - Recording paused public static var voiceBroadcastRecorderConnectionError: String { return VectorL10n.tr("Vector", "voice_broadcast_recorder_connection_error") diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 422e08990..77ba974fd 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -1053,8 +1053,13 @@ static NSString *const kRepliedTextPattern = @".*
.*
(.* else if ([event.decryptionError.domain isEqualToString:MXDecryptingErrorDomain] && event.decryptionError.code == MXDecryptingErrorUnknownInboundSessionIdCode) { - // Make the unknown inbound session id error description more user friendly - errorDescription = [VectorL10n noticeCryptoErrorUnknownInboundSessionId]; + // Hide the decryption error for event related to another one (like voicebroadcast chunks) + if ([event.relatesTo.relationType isEqualToString:MXEventRelationTypeReference]) { + displayText = nil; + } else { + // Make the unknown inbound session id error description more user friendly + errorDescription = [VectorL10n noticeCryptoErrorUnknownInboundSessionId]; + } } else if ([event.decryptionError.domain isEqualToString:MXDecryptingErrorDomain] && event.decryptionError.code == MXDecryptingErrorDuplicateMessageIndexCode) diff --git a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift index 39264b42c..4522df16c 100644 --- a/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift +++ b/Riot/Modules/VoiceBroadcast/VoiceBroadcastSDK/VoiceBroadcastAggregator.swift @@ -35,6 +35,7 @@ public protocol VoiceBroadcastAggregatorDelegate: AnyObject { func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveChunk: VoiceBroadcastChunk) func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didReceiveState: VoiceBroadcastInfoState) func voiceBroadcastAggregatorDidUpdateData(_ aggregator: VoiceBroadcastAggregator) + func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didUpdateUndecryptableEventList events: Set) } /** @@ -58,6 +59,7 @@ public class VoiceBroadcastAggregator { private var referenceEventsListener: Any? private var events: [MXEvent] = [] + private var undecryptableEvents: Set = [] public private(set) var voiceBroadcast: VoiceBroadcast! { didSet { @@ -84,7 +86,7 @@ public class VoiceBroadcastAggregator { try buildVoiceBroadcastStartContent() } - + private func buildVoiceBroadcastStartContent() throws { guard let event = session.store.event(withEventId: voiceBroadcastStartEventId, inRoom: room.roomId), let eventContent = VoiceBroadcastInfo(fromJSON: event.content), @@ -118,7 +120,11 @@ public class VoiceBroadcastAggregator { @objc private func eventDidDecrypt(sender: Notification) { guard let event = sender.object as? MXEvent else { return } - + + if undecryptableEvents.remove(event) != nil { + delegate?.voiceBroadcastAggregator(self, didUpdateUndecryptableEventList: undecryptableEvents) + } + self.handleEvent(event: event) } @@ -138,8 +144,19 @@ public class VoiceBroadcastAggregator { private func updateVoiceBroadcast(event: MXEvent) { guard event.sender == self.voiceBroadcastSenderId, let relatedEventId = event.relatesTo?.eventId, - relatedEventId == self.voiceBroadcastStartEventId, - event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil else { + relatedEventId == self.voiceBroadcastStartEventId else { + return + } + + // Handle decryption errors + if event.decryptionError != nil { + self.undecryptableEvents.insert(event) + self.delegate?.voiceBroadcastAggregator(self, didUpdateUndecryptableEventList: self.undecryptableEvents) + + return + } + + guard event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil else { return } @@ -192,15 +209,22 @@ public class VoiceBroadcastAggregator { } self.events.removeAll() + self.undecryptableEvents.removeAll() self.voiceBroadcastLastChunkSequence = 0 let filteredChunk = response.chunk.filter { event in event.sender == self.voiceBroadcastSenderId && event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil } - self.events.append(contentsOf: filteredChunk) - + + let decryptionFailure = response.chunk.filter { event in + event.sender == self.voiceBroadcastSenderId && + event.decryptionError != nil + } + self.undecryptableEvents.formUnion(decryptionFailure) + self.delegate?.voiceBroadcastAggregator(self, didUpdateUndecryptableEventList: self.undecryptableEvents) + let eventTypes = [VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType, kMXEventTypeStringRoomMessage] self.referenceEventsListener = self.room.listen(toEventsOfTypes: eventTypes, onEvent: { [weak self] event, direction, roomState in self?.handleEvent(event: event, direction: direction, roomState: roomState) diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift index ffe713e73..fd1ca9157 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/MatrixSDK/VoiceBroadcastPlaybackViewModel.swift @@ -111,7 +111,8 @@ class VoiceBroadcastPlaybackViewModel: VoiceBroadcastPlaybackViewModelType, Voic broadcastState: voiceBroadcastAggregator.voiceBroadcastState, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: Float(voiceBroadcastAggregator.voiceBroadcast.duration), isLive: false, canMoveForward: false, canMoveBackward: false), - bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0)) + bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0), + decryptionState: VoiceBroadcastPlaybackDecryptionState(errorCount: 0)) super.init(initialViewState: viewState) displayLink = CADisplayLink(target: WeakTarget(self, selector: #selector(handleDisplayLinkTick)), selector: WeakTarget.triggerSelector) @@ -486,6 +487,17 @@ extension VoiceBroadcastPlaybackViewModel: VoiceBroadcastAggregatorDelegate { handleVoiceBroadcastChunksProcessing() } } + + func voiceBroadcastAggregator(_ aggregator: VoiceBroadcastAggregator, didUpdateUndecryptableEventList events: Set) { + state.decryptionState.errorCount = events.count + if events.count > 0 { + MXLog.debug("[VoiceBroadcastPlaybackViewModel] voice broadcast decryption error count: \(events.count)/\(aggregator.voiceBroadcast.chunks.count)") + + if [.playing, .buffering].contains(state.playbackState) { + pause() + } + } + } } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackDecryptionErrorView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackDecryptionErrorView.swift new file mode 100644 index 000000000..598bde5c3 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackDecryptionErrorView.swift @@ -0,0 +1,47 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct VoiceBroadcastPlaybackDecryptionErrorView: View { + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + var body: some View { + ZStack { + HStack(spacing: 0) { + Image(uiImage: Asset.Images.errorIcon.image) + .frame(width: 40, height: 40) + Text(VectorL10n.voiceBroadcastPlaybackUnableToDecrypt) + .multilineTextAlignment(.center) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.alert) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } +} + +struct VoiceBroadcastPlaybackDecryptionErrorView_Previews: PreviewProvider { + static var previews: some View { + VoiceBroadcastPlaybackDecryptionErrorView() + } +} diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift index 09ed1ff44..6ac146ce2 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/View/VoiceBroadcastPlaybackView.swift @@ -91,7 +91,7 @@ struct VoiceBroadcastPlaybackView: View { } } }.frame(maxWidth: .infinity, alignment: .leading) - + if viewModel.viewState.broadcastState != .stopped { Label { Text(VectorL10n.voiceBroadcastLive) @@ -109,7 +109,12 @@ struct VoiceBroadcastPlaybackView: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 4.0, trailing: 0.0)) - if viewModel.viewState.playbackState == .error { + if viewModel.viewState.decryptionState.errorCount > 0 { + VoiceBroadcastPlaybackDecryptionErrorView() + .fixedSize(horizontal: false, vertical: true) + .accessibilityIdentifier("decryptionErrorView") + } + else if viewModel.viewState.playbackState == .error { VoiceBroadcastPlaybackErrorView() } else { HStack (spacing: 34.0) { @@ -156,8 +161,8 @@ struct VoiceBroadcastPlaybackView: View { } VoiceBroadcastSlider(value: $viewModel.progress, - minValue: 0.0, - maxValue: viewModel.viewState.playingState.duration) { didChange in + minValue: 0.0, + maxValue: viewModel.viewState.playingState.duration) { didChange in viewModel.send(viewAction: .sliderChange(didChange: didChange)) } diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift index 488b65c1d..aeb1f4f61 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackModels.swift @@ -48,12 +48,17 @@ struct VoiceBroadcastPlayingState { var canMoveBackward: Bool } +struct VoiceBroadcastPlaybackDecryptionState { + var errorCount: Int +} + struct VoiceBroadcastPlaybackViewState: BindableState { var details: VoiceBroadcastPlaybackDetails var broadcastState: VoiceBroadcastInfoState var playbackState: VoiceBroadcastPlaybackState var playingState: VoiceBroadcastPlayingState var bindings: VoiceBroadcastPlaybackViewStateBindings + var decryptionState: VoiceBroadcastPlaybackDecryptionState } struct VoiceBroadcastPlaybackViewStateBindings { diff --git a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift index 306a5be8c..84f210d81 100644 --- a/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift +++ b/RiotSwiftUI/Modules/Room/VoiceBroadcastPlayback/VoiceBroadcastPlaybackScreenState.swift @@ -43,11 +43,12 @@ enum MockVoiceBroadcastPlaybackScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { let details = VoiceBroadcastPlaybackDetails(senderDisplayName: "Alice", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room")) - let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .started, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: 10.0, isLive: true, canMoveForward: false, canMoveBackward: false), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0))) + let viewModel = MockVoiceBroadcastPlaybackViewModel(initialViewState: VoiceBroadcastPlaybackViewState(details: details, broadcastState: .started, playbackState: .stopped, playingState: VoiceBroadcastPlayingState(duration: 10.0, isLive: true, canMoveForward: false, canMoveBackward: false), bindings: VoiceBroadcastPlaybackViewStateBindings(progress: 0), decryptionState: VoiceBroadcastPlaybackDecryptionState(errorCount: 0))) return ( [false, viewModel], - AnyView(VoiceBroadcastPlaybackView(viewModel: viewModel.context)) + AnyView(VoiceBroadcastPlaybackView(viewModel: viewModel.context) + .environmentObject(AvatarViewModel.withMockedServices())) ) } } diff --git a/changelog.d/7189.change b/changelog.d/7189.change new file mode 100644 index 000000000..a9acc4ba3 --- /dev/null +++ b/changelog.d/7189.change @@ -0,0 +1 @@ +Voice Broadcast: Inform the user about decryption errors during a voice broadcast.