Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Warn and block sending on verification violation #3679

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4682,6 +4682,76 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
return pinUserIdentityReturnValue
}
}
//MARK: - withdrawUserIdentityVerification

var withdrawUserIdentityVerificationUnderlyingCallsCount = 0
var withdrawUserIdentityVerificationCallsCount: Int {
get {
if Thread.isMainThread {
return withdrawUserIdentityVerificationUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = withdrawUserIdentityVerificationUnderlyingCallsCount
}

return returnValue!
}
}
set {
if Thread.isMainThread {
withdrawUserIdentityVerificationUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
withdrawUserIdentityVerificationUnderlyingCallsCount = newValue
}
}
}
}
var withdrawUserIdentityVerificationCalled: Bool {
return withdrawUserIdentityVerificationCallsCount > 0
}
var withdrawUserIdentityVerificationReceivedUserID: String?
var withdrawUserIdentityVerificationReceivedInvocations: [String] = []

var withdrawUserIdentityVerificationUnderlyingReturnValue: Result<Void, ClientProxyError>!
var withdrawUserIdentityVerificationReturnValue: Result<Void, ClientProxyError>! {
get {
if Thread.isMainThread {
return withdrawUserIdentityVerificationUnderlyingReturnValue
} else {
var returnValue: Result<Void, ClientProxyError>? = nil
DispatchQueue.main.sync {
returnValue = withdrawUserIdentityVerificationUnderlyingReturnValue
}

return returnValue!
}
}
set {
if Thread.isMainThread {
withdrawUserIdentityVerificationUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
withdrawUserIdentityVerificationUnderlyingReturnValue = newValue
}
}
}
}
var withdrawUserIdentityVerificationClosure: ((String) async -> Result<Void, ClientProxyError>)?

func withdrawUserIdentityVerification(_ userID: String) async -> Result<Void, ClientProxyError> {
withdrawUserIdentityVerificationCallsCount += 1
withdrawUserIdentityVerificationReceivedUserID = userID
DispatchQueue.main.async {
self.withdrawUserIdentityVerificationReceivedInvocations.append(userID)
}
if let withdrawUserIdentityVerificationClosure = withdrawUserIdentityVerificationClosure {
return await withdrawUserIdentityVerificationClosure(userID)
} else {
return withdrawUserIdentityVerificationReturnValue
}
}
//MARK: - resetIdentity

var resetIdentityUnderlyingCallsCount = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ enum ComposerAttachmentType {
struct ComposerToolbarViewState: BindableState {
var composerMode: ComposerMode = .default
var composerEmpty = true
/// Could be false if sending is disabled in the room
var canSend = true
var suggestions: [SuggestionItem] = []
var audioPlayerState: AudioPlayerState
var audioRecorderState: AudioRecorderState
Expand Down Expand Up @@ -97,6 +99,10 @@ struct ComposerToolbarViewState: BindableState {
}

var sendButtonDisabled: Bool {
if !canSend {
return true
}

if case .previewVoiceMessage = composerMode {
return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
private var initialText: String?
private let wysiwygViewModel: WysiwygComposerViewModel
private let completionSuggestionService: CompletionSuggestionServiceProtocol
private let roomProxy: JoinedRoomProxyProtocol
private let analyticsService: AnalyticsService
private let draftService: ComposerDraftServiceProtocol
private var identityPinningViolations = [String: RoomMemberProxyProtocol]()

private let mentionBuilder: MentionBuilderProtocol
private let attributedStringBuilder: AttributedStringBuilderProtocol
Expand All @@ -43,6 +45,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
private var replyLoadingTask: Task<Void, Never>?

init(initialText: String? = nil,
roomProxy: JoinedRoomProxyProtocol,
wysiwygViewModel: WysiwygComposerViewModel,
completionSuggestionService: CompletionSuggestionServiceProtocol,
mediaProvider: MediaProviderProtocol,
Expand All @@ -53,6 +56,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
self.wysiwygViewModel = wysiwygViewModel
self.completionSuggestionService = completionSuggestionService
self.analyticsService = analyticsService
self.roomProxy = roomProxy
draftService = composerDraftService

mentionBuilder = MentionBuilder()
Expand Down Expand Up @@ -120,6 +124,19 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool

setupMentionsHandling(mentionDisplayHelper: mentionDisplayHelper)
focusComposerIfHardwareKeyboardConnected()

let identityStatusChangesPublisher = roomProxy.identityStatusChangesPublisher.receive(on: DispatchQueue.main)

Task { [weak self] in
for await changes in identityStatusChangesPublisher.values {
guard !Task.isCancelled else {
return
}

await self?.processIdentityStatusChanges(changes)
}
}
.store(in: &cancellables)
}

// MARK: - Public
Expand Down Expand Up @@ -477,6 +494,25 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
}
}
}

private func processIdentityStatusChanges(_ changes: [IdentityStatusChange]) async {
for change in changes {
switch change.changedTo {
case .verificationViolation:
guard case let .success(member) = await roomProxy.getMember(userID: change.userId) else {
MXLog.error("Failed retrieving room member for identity status change: \(change)")
continue
}

identityPinningViolations[change.userId] = member
default:
// clear
identityPinningViolations[change.userId] = nil
}
}

state.canSend = identityPinningViolations.isEmpty
}

private func set(mode: ComposerMode) {
if state.composerMode.isLoadingReply, state.composerMode.replyEventID != mode.replyEventID {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
// Please see LICENSE files in the repository root for full details.
//

import Combine
import Compound
import MatrixRustSDK
import SwiftUI
import WysiwygComposer

Expand Down Expand Up @@ -44,6 +46,7 @@ struct ComposerToolbar: View {
.offset(y: -frame.height)
}
}
.disabled(!context.viewState.canSend)
.alert(item: $context.alertInfo)
}

Expand Down Expand Up @@ -297,7 +300,7 @@ struct ComposerToolbar: View {

struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
static let wysiwygViewModel = WysiwygComposerViewModel()
static let composerViewModel = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
static let composerViewModel = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()), wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init(suggestions: suggestions)),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
Expand Down Expand Up @@ -331,14 +334,19 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
ComposerToolbar.replyLoadingPreviewMock(isLoading: false)
}
.previewDisplayName("Reply")

VStack(spacing: 8) {
ComposerToolbar.disabledPreviewMock()
}
.previewDisplayName("Disabled")
}
}

extension ComposerToolbar {
static func mock(focused: Bool = true) -> ComposerToolbar {
let wysiwygViewModel = WysiwygComposerViewModel()
var composerViewModel: ComposerToolbarViewModel {
let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
let model = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()), wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
Expand All @@ -355,7 +363,7 @@ extension ComposerToolbar {
static func textWithVoiceMessage(focused: Bool = true) -> ComposerToolbar {
let wysiwygViewModel = WysiwygComposerViewModel()
var composerViewModel: ComposerToolbarViewModel {
let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
let model = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()), wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
Expand All @@ -372,7 +380,7 @@ extension ComposerToolbar {
static func voiceMessageRecordingMock() -> ComposerToolbar {
let wysiwygViewModel = WysiwygComposerViewModel()
var composerViewModel: ComposerToolbarViewModel {
let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
let model = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()), wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
Expand All @@ -390,7 +398,7 @@ extension ComposerToolbar {
let wysiwygViewModel = WysiwygComposerViewModel()
let waveformData: [Float] = Array(repeating: 1.0, count: 1000)
var composerViewModel: ComposerToolbarViewModel {
let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
let model = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()), wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
Expand All @@ -411,7 +419,7 @@ extension ComposerToolbar {
static func replyLoadingPreviewMock(isLoading: Bool) -> ComposerToolbar {
let wysiwygViewModel = WysiwygComposerViewModel()
var composerViewModel: ComposerToolbarViewModel {
let model = ComposerToolbarViewModel(wysiwygViewModel: wysiwygViewModel,
let model = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()), wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
Expand All @@ -430,4 +438,21 @@ extension ComposerToolbar {
wysiwygViewModel: wysiwygViewModel,
keyCommands: [])
}

static func disabledPreviewMock() -> ComposerToolbar {
let wysiwygViewModel = WysiwygComposerViewModel()
var composerViewModel: ComposerToolbarViewModel {
let model = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()), wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
analyticsService: ServiceLocator.shared.analytics,
composerDraftService: ComposerDraftServiceMock())
model.state.canSend = false
return model
}
return ComposerToolbar(context: composerViewModel.context,
wysiwygViewModel: wysiwygViewModel,
keyCommands: [])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import WysiwygComposer
struct RoomAttachmentPicker: View {
@ObservedObject var context: ComposerToolbarViewModel.Context

@Environment(\.isEnabled) private var isEnabled

var body: some View {
// Use a menu instead of the popover/sheet shown in Figma because overriding the colour scheme
// results in a rendering bug on 17.1: https://github.com/element-hq/element-x-ios/issues/2157
Expand All @@ -20,6 +22,9 @@ struct RoomAttachmentPicker: View {
} label: {
CompoundIcon(asset: Asset.Images.composerAttachment, size: .custom(30), relativeTo: .compound.headingLG)
.scaledPadding(7, relativeTo: .compound.headingLG)
.foregroundColor(
isEnabled ? .compound.iconPrimary : .compound.iconDisabled
)
}
.buttonStyle(RoomAttachmentPickerButtonStyle())
.accessibilityLabel(L10n.actionAddToTimeline)
Expand Down Expand Up @@ -81,7 +86,8 @@ private struct RoomAttachmentPickerButtonStyle: ButtonStyle {
}

struct RoomAttachmentPicker_Previews: PreviewProvider, TestablePreview {
static let viewModel = ComposerToolbarViewModel(wysiwygViewModel: WysiwygComposerViewModel(),
static let viewModel = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
wysiwygViewModel: WysiwygComposerViewModel(),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
mediaProvider: MediaProviderMock(configuration: .init()),
mentionDisplayHelper: ComposerMentionDisplayHelper.mock,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ enum VoiceMessageRecordingButtonMode {
}

struct VoiceMessageRecordingButton: View {
@Environment(\.isEnabled) private var isEnabled

let mode: VoiceMessageRecordingButtonMode
var startRecording: (() -> Void)?
var stopRecording: (() -> Void)?
Expand All @@ -33,7 +35,9 @@ struct VoiceMessageRecordingButton: View {
switch mode {
case .idle:
CompoundIcon(\.micOn, size: .medium, relativeTo: .compound.headingLG)
.foregroundColor(.compound.iconSecondary)
.foregroundColor(
isEnabled ? .compound.iconSecondary : .compound.iconDisabled
)
.scaledPadding(10, relativeTo: .compound.headingLG)
case .recording:
CompoundIcon(asset: Asset.Images.stopRecording, size: .medium, relativeTo: .compound.headingLG)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
maxCompressedHeight: ComposerConstant.maxHeight,
maxExpandedHeight: ComposerConstant.maxHeight,
parserStyle: .elementX)
let composerViewModel = ComposerToolbarViewModel(initialText: parameters.sharedText,
let composerViewModel = ComposerToolbarViewModel(initialText: parameters.sharedText, roomProxy: parameters.roomProxy,
wysiwygViewModel: wysiwygViewModel,
completionSuggestionService: parameters.completionSuggestionService,
mediaProvider: parameters.mediaProvider,
Expand Down
2 changes: 2 additions & 0 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@ struct RoomScreenViewStateBindings { }

enum RoomScreenFooterViewAction {
case resolvePinViolation(userID: String)
case resolveVerificationViolation(userID: String)
}

enum RoomScreenFooterViewDetails {
case pinViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL)
case verificationViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL)
}

enum PinnedEventsBannerState: Equatable {
Expand Down
Loading
Loading