Inform the user about decryption errors during a voice broadcast

This commit is contained in:
Nicolas Mauri 2023-01-23 16:17:33 +01:00
parent 971a9f0cfe
commit c0e5697f07
10 changed files with 120 additions and 15 deletions

View file

@ -2225,6 +2225,7 @@ Tap the + to start adding people.";
"voice_broadcast_connection_error_title" = "Connection error";
"voice_broadcast_connection_error_message" = "Unfortunately were 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

View file

@ -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")

View file

@ -1053,8 +1053,13 @@ static NSString *const kRepliedTextPattern = @"<mx-reply>.*<blockquote>.*<br>(.*
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)

View file

@ -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<MXEvent>)
}
/**
@ -58,6 +59,7 @@ public class VoiceBroadcastAggregator {
private var referenceEventsListener: Any?
private var events: [MXEvent] = []
private var undecryptableEvents: Set<MXEvent> = []
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)

View file

@ -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<MXEvent>) {
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()
}
}
}
}

View file

@ -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()
}
}

View file

@ -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))
}

View file

@ -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 {

View file

@ -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()))
)
}
}

1
changelog.d/7189.change Normal file
View file

@ -0,0 +1 @@
Voice Broadcast: Inform the user about decryption errors during a voice broadcast.