mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Require acknowledgement to send to verified users who have unsigned devices or have changed their identity. (#3215)
* Refactor HeroImage style. * Add a screen to resolve (crypto-related) timeline item send failures. * Refactor send failures. * Update the SDK.
This commit is contained in:
+65
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// Copyright 2022 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 Combine
|
||||
import SwiftUI
|
||||
|
||||
struct ResolveVerifiedUserSendFailureScreenCoordinatorParameters {
|
||||
let failure: TimelineItemSendFailure.VerifiedUser
|
||||
let itemID: TimelineItemIdentifier
|
||||
let roomProxy: JoinedRoomProxyProtocol
|
||||
}
|
||||
|
||||
enum ResolveVerifiedUserSendFailureScreenCoordinatorAction {
|
||||
case dismiss
|
||||
}
|
||||
|
||||
final class ResolveVerifiedUserSendFailureScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: ResolveVerifiedUserSendFailureScreenCoordinatorParameters
|
||||
private let viewModel: ResolveVerifiedUserSendFailureScreenViewModelProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<ResolveVerifiedUserSendFailureScreenCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<ResolveVerifiedUserSendFailureScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: ResolveVerifiedUserSendFailureScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = ResolveVerifiedUserSendFailureScreenViewModel(failure: parameters.failure,
|
||||
itemID: parameters.itemID,
|
||||
roomProxy: parameters.roomProxy)
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actionsPublisher.sink { [weak self] action in
|
||||
MXLog.info("Coordinator: received view model action: \(action)")
|
||||
|
||||
guard let self else { return }
|
||||
switch action {
|
||||
case .dismiss:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(ResolveVerifiedUserSendFailureScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Copyright 2022 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 Foundation
|
||||
|
||||
enum ResolveVerifiedUserSendFailureScreenViewModelAction {
|
||||
case dismiss
|
||||
}
|
||||
|
||||
struct ResolveVerifiedUserSendFailureScreenViewState: BindableState {
|
||||
var currentFailure: TimelineItemSendFailure.VerifiedUser
|
||||
var currentMemberDisplayName: String
|
||||
|
||||
var title: String {
|
||||
switch currentFailure {
|
||||
case .hasUnsignedDevice: L10n.screenResolveSendFailureUnsignedDeviceTitle(currentMemberDisplayName)
|
||||
case .changedIdentity: L10n.screenResolveSendFailureChangedIdentityTitle(currentMemberDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
var subtitle: String {
|
||||
switch currentFailure {
|
||||
case .hasUnsignedDevice: L10n.screenResolveSendFailureUnsignedDeviceSubtitle(currentMemberDisplayName, currentMemberDisplayName)
|
||||
case .changedIdentity: L10n.screenResolveSendFailureChangedIdentitySubtitle(currentMemberDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
var primaryButtonTitle: String {
|
||||
switch currentFailure {
|
||||
case .hasUnsignedDevice: L10n.screenResolveSendFailureUnsignedDevicePrimaryButtonTitle
|
||||
case .changedIdentity: L10n.screenResolveSendFailureChangedIdentityPrimaryButtonTitle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ResolveVerifiedUserSendFailureScreenViewAction {
|
||||
case resolveAndResend
|
||||
case resend
|
||||
case cancel
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// Copyright 2022 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 Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias ResolveVerifiedUserSendFailureScreenViewModelType = StateStoreViewModel<ResolveVerifiedUserSendFailureScreenViewState, ResolveVerifiedUserSendFailureScreenViewAction>
|
||||
|
||||
class ResolveVerifiedUserSendFailureScreenViewModel: ResolveVerifiedUserSendFailureScreenViewModelType, ResolveVerifiedUserSendFailureScreenViewModelProtocol {
|
||||
private let iterator: VerifiedUserSendFailureIterator
|
||||
private let failure: TimelineItemSendFailure.VerifiedUser
|
||||
private let itemID: TimelineItemIdentifier
|
||||
private let roomProxy: JoinedRoomProxyProtocol
|
||||
private var members: [String: RoomMemberProxyProtocol]
|
||||
|
||||
private let actionsSubject: PassthroughSubject<ResolveVerifiedUserSendFailureScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<ResolveVerifiedUserSendFailureScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier, roomProxy: JoinedRoomProxyProtocol) {
|
||||
iterator = switch failure {
|
||||
case .hasUnsignedDevice(let devices): UnsignedDeviceFailureIterator(devices: devices)
|
||||
case .changedIdentity(let users): ChangedIdentityFailureIterator(users: users)
|
||||
}
|
||||
|
||||
self.failure = failure
|
||||
self.itemID = itemID
|
||||
self.roomProxy = roomProxy
|
||||
|
||||
members = Dictionary(uniqueKeysWithValues: roomProxy.membersPublisher.value.map { ($0.userID, $0) })
|
||||
|
||||
guard let (userID, failure) = iterator.next() else {
|
||||
MXLog.error("There aren't any known users/devices to resolve the failure with.")
|
||||
fatalError("There aren't any known users/devices to resolve the failure with.")
|
||||
}
|
||||
|
||||
super.init(initialViewState: ResolveVerifiedUserSendFailureScreenViewState(currentFailure: failure,
|
||||
currentMemberDisplayName: members[userID]?.displayName ?? userID))
|
||||
}
|
||||
|
||||
// MARK: Public
|
||||
|
||||
override func process(viewAction: ResolveVerifiedUserSendFailureScreenViewAction) {
|
||||
MXLog.info("View model: received view action: \(viewAction)")
|
||||
|
||||
switch viewAction {
|
||||
case .resolveAndResend:
|
||||
Task { await resolveAndResend() }
|
||||
case .resend:
|
||||
Task { await resend() }
|
||||
case .cancel:
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveAndResend() async {
|
||||
let result = switch failure {
|
||||
case .hasUnsignedDevice(let devices):
|
||||
await roomProxy.ignoreDeviceTrustAndResend(devices: devices, itemID: itemID)
|
||||
case .changedIdentity(let users):
|
||||
await roomProxy.withdrawVerificationAndResend(userIDs: users, itemID: itemID)
|
||||
}
|
||||
|
||||
if case let .failure(error) = result {
|
||||
#warning("Show the error?")
|
||||
return
|
||||
}
|
||||
|
||||
if let (userID, failure) = iterator.next() {
|
||||
state.currentMemberDisplayName = members[userID]?.displayName ?? userID
|
||||
state.currentFailure = failure
|
||||
} else {
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
private func resend() async {
|
||||
switch await roomProxy.resend(itemID: itemID) {
|
||||
case .success:
|
||||
actionsSubject.send(.dismiss)
|
||||
case .failure(let failure):
|
||||
#warning("Show the error?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Iterators
|
||||
|
||||
@MainActor
|
||||
private protocol VerifiedUserSendFailureIterator {
|
||||
func next() -> (userID: String, failure: TimelineItemSendFailure.VerifiedUser)?
|
||||
}
|
||||
|
||||
private class UnsignedDeviceFailureIterator: VerifiedUserSendFailureIterator {
|
||||
private var iterator: [String: [String]].Iterator
|
||||
|
||||
init(devices: [String: [String]]) {
|
||||
iterator = devices.makeIterator()
|
||||
}
|
||||
|
||||
func next() -> (userID: String, failure: TimelineItemSendFailure.VerifiedUser)? {
|
||||
guard let nextUserDevices = iterator.next() else { return nil }
|
||||
return (nextUserDevices.key, .hasUnsignedDevice(devices: [nextUserDevices.key: nextUserDevices.value]))
|
||||
}
|
||||
}
|
||||
|
||||
private class ChangedIdentityFailureIterator: VerifiedUserSendFailureIterator {
|
||||
private var iterator: [String].Iterator
|
||||
|
||||
init(users: [String]) {
|
||||
iterator = users.makeIterator()
|
||||
}
|
||||
|
||||
func next() -> (userID: String, failure: TimelineItemSendFailure.VerifiedUser)? {
|
||||
guard let nextUserID = iterator.next() else { return nil }
|
||||
return (nextUserID, .changedIdentity(users: [nextUserID]))
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright 2022 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 Combine
|
||||
|
||||
@MainActor
|
||||
protocol ResolveVerifiedUserSendFailureScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<ResolveVerifiedUserSendFailureScreenViewModelAction, Never> { get }
|
||||
var context: ResolveVerifiedUserSendFailureScreenViewModelType.Context { get }
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// Copyright 2022 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 Compound
|
||||
import SwiftUI
|
||||
|
||||
struct ResolveVerifiedUserSendFailureScreen: View {
|
||||
@ObservedObject var context: ResolveVerifiedUserSendFailureScreenViewModel.Context
|
||||
@State private var sheetFrame: CGRect = .zero
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 40) {
|
||||
header
|
||||
buttons
|
||||
}
|
||||
.padding(.top, 24)
|
||||
.padding(.bottom, 16)
|
||||
.padding(.horizontal, 16)
|
||||
.readFrame($sheetFrame)
|
||||
}
|
||||
.scrollBounceBehavior(.basedOnSize)
|
||||
.presentationDetents([.height(sheetFrame.height)])
|
||||
}
|
||||
|
||||
var header: some View {
|
||||
VStack(spacing: 8) {
|
||||
HeroImage(icon: \.error, style: .critical)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(context.viewState.title)
|
||||
.font(.compound.headingMDBold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
|
||||
Text(context.viewState.subtitle)
|
||||
.font(.compound.bodyMD)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
var buttons: some View {
|
||||
VStack(spacing: 16) {
|
||||
Button(context.viewState.primaryButtonTitle) {
|
||||
context.send(viewAction: .resolveAndResend)
|
||||
}
|
||||
.buttonStyle(.compound(.primary))
|
||||
|
||||
Button(L10n.actionRetry) {
|
||||
context.send(viewAction: .resend)
|
||||
}
|
||||
.buttonStyle(.compound(.secondary))
|
||||
|
||||
Button { context.send(viewAction: .cancel) } label: {
|
||||
Text(L10n.actionCancelForNow)
|
||||
.padding(.vertical, 14)
|
||||
}
|
||||
.buttonStyle(.compound(.plain))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct ResolveVerifiedUserSendFailureScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let unsignedDeviceViewModel = makeViewModel(failure: .hasUnsignedDevice(devices: ["@alice:matrix.org": []]))
|
||||
static let changedIdentityViewModel = makeViewModel(failure: .changedIdentity(users: ["@alice:matrix.org"]))
|
||||
|
||||
static var previews: some View {
|
||||
ResolveVerifiedUserSendFailureScreen(context: unsignedDeviceViewModel.context)
|
||||
.previewDisplayName("Unsigned Device")
|
||||
|
||||
ResolveVerifiedUserSendFailureScreen(context: changedIdentityViewModel.context)
|
||||
.previewDisplayName("Identity Changed")
|
||||
}
|
||||
|
||||
static func makeViewModel(failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureScreenViewModel {
|
||||
ResolveVerifiedUserSendFailureScreenViewModel(failure: failure,
|
||||
itemID: .random,
|
||||
roomProxy: JoinedRoomProxyMock(.init()))
|
||||
}
|
||||
}
|
||||
|
||||
struct ResolveVerifiedUserSendFailureScreenSheet_Previews: PreviewProvider {
|
||||
static let viewModel = ResolveVerifiedUserSendFailureScreenViewModel(failure: .changedIdentity(users: ["@alice:matrix.org"]),
|
||||
itemID: .random,
|
||||
roomProxy: JoinedRoomProxyMock(.init()))
|
||||
|
||||
static var previews: some View {
|
||||
Text("Hello")
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
ResolveVerifiedUserSendFailureScreen(context: viewModel.context)
|
||||
}
|
||||
.previewDisplayName("Sheet")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user