// // CIRcvDecryptionError.swift // SimpleX (iOS) // // Created by Evgeny on 15/04/2023. // Copyright © 2023 SimpleX Chat. All rights reserved. // import SwiftUI import SimpleXChat let decryptErrorReason: LocalizedStringKey = "It can happen when you or your connection used the old database backup." struct CIRcvDecryptionError: View { @EnvironmentObject var m: ChatModel @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat @Environment(\.showTimestamp) var showTimestamp: Bool var msgDecryptError: MsgDecryptError var msgCount: UInt32 var chatItem: ChatItem @State private var alert: CIRcvDecryptionErrorAlert? @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false enum CIRcvDecryptionErrorAlert: Identifiable { case syncAllowedAlert(_ syncConnection: () -> Void) case syncNotSupportedContactAlert case syncNotSupportedMemberAlert case decryptionErrorAlert case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { case .syncAllowedAlert: return "syncAllowedAlert" case .syncNotSupportedContactAlert: return "syncNotSupportedContactAlert" case .syncNotSupportedMemberAlert: return "syncNotSupportedMemberAlert" case .decryptionErrorAlert: return "decryptionErrorAlert" case let .error(title, _): return "error \(title)" } } } var body: some View { viewBody() .onAppear { // for direct chat ConnectionStats are populated on opening chat, see ChatView onAppear if case let .group(groupInfo) = chat.chatInfo, case let .groupRcv(groupMember) = chatItem.chatDir { do { let (member, stats) = try apiGroupMemberInfoSync(groupInfo.apiId, groupMember.groupMemberId) if let s = stats { m.updateGroupMemberConnectionStats(groupInfo, member, s) } } catch let error { logger.error("apiGroupMemberInfo error: \(responseError(error))") } } } .alert(item: $alert) { alertItem in switch(alertItem) { case let .syncAllowedAlert(syncConnection): return syncAllowedAlert(syncConnection) case .syncNotSupportedContactAlert: return Alert(title: Text("Fix not supported by contact"), message: message()) case .syncNotSupportedMemberAlert: return Alert(title: Text("Fix not supported by group member"), message: message()) case .decryptionErrorAlert: return Alert(title: Text("Decryption error"), message: message()) case let .error(title, error): return mkAlert(title: title, message: error) } } } @ViewBuilder private func viewBody() -> some View { Group { if case let .direct(contact) = chat.chatInfo, let contactStats = contact.activeConn?.connectionStats { if contactStats.ratchetSyncAllowed { decryptionErrorItemFixButton(syncSupported: true) { alert = .syncAllowedAlert { syncContactConnection(contact) } } } else if !contactStats.ratchetSyncSupported { decryptionErrorItemFixButton(syncSupported: false) { alert = .syncNotSupportedContactAlert } } else { basicDecryptionErrorItem() } } else if case let .group(groupInfo) = chat.chatInfo, case let .groupRcv(groupMember) = chatItem.chatDir, let mem = m.getGroupMember(groupMember.groupMemberId), let memberStats = mem.wrapped.activeConn?.connectionStats { if memberStats.ratchetSyncAllowed { decryptionErrorItemFixButton(syncSupported: true) { alert = .syncAllowedAlert { syncMemberConnection(groupInfo, groupMember) } } } else if !memberStats.ratchetSyncSupported { decryptionErrorItemFixButton(syncSupported: false) { alert = .syncNotSupportedMemberAlert } } else { basicDecryptionErrorItem() } } else { basicDecryptionErrorItem() } } .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) } } private func basicDecryptionErrorItem() -> some View { decryptionErrorItem { alert = .decryptionErrorAlert } } private func decryptionErrorItemFixButton(syncSupported: Bool, _ onClick: @escaping (() -> Void)) -> some View { ZStack(alignment: .bottomTrailing) { VStack(alignment: .leading, spacing: 2) { HStack { Text(chatItem.content.text) .foregroundColor(.red) .italic() } ( Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath")) .foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary) .font(.callout) + textSpace + Text("Fix connection") .foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary) .font(.callout) + Text(verbatim: " ") + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) ) } .padding(.horizontal, 12) CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) .padding(.horizontal, 12) } .simultaneousGesture(TapGesture().onEnded(onClick)) .padding(.vertical, 6) .textSelection(.disabled) } private func decryptionErrorItem(_ onClick: @escaping (() -> Void)) -> some View { return ZStack(alignment: .bottomTrailing) { HStack { Text(chatItem.content.text) .foregroundColor(.red) .italic() + Text(verbatim: " ") + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp) } .padding(.horizontal, 12) CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) .padding(.horizontal, 12) } .simultaneousGesture(TapGesture().onEnded(onClick)) .padding(.vertical, 6) .textSelection(.disabled) } private func message() -> Text { var message: Text let why = Text(decryptErrorReason) switch msgDecryptError { case .ratchetHeader: message = Text("\(msgCount) messages failed to decrypt.") + textNewLine + why case .tooManySkipped: message = Text("\(msgCount) messages skipped.") + textNewLine + why case .ratchetEarlier: message = Text("\(msgCount) messages failed to decrypt.") + textNewLine + why case .other: message = Text("\(msgCount) messages failed to decrypt.") + textNewLine + why case .ratchetSync: message = Text("Encryption re-negotiation failed.") } return message } private func syncMemberConnection(_ groupInfo: GroupInfo, _ member: GroupMember) { Task { do { let (mem, stats) = try apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, false) await MainActor.run { m.updateGroupMemberConnectionStats(groupInfo, mem, stats) } } catch let error { logger.error("syncMemberConnection apiSyncGroupMemberRatchet error: \(responseError(error))") let a = getErrorAlert(error, "Error synchronizing connection") await MainActor.run { alert = .error(title: a.title, error: a.message) } } } } private func syncContactConnection(_ contact: Contact) { Task { do { let stats = try apiSyncContactRatchet(contact.apiId, false) await MainActor.run { m.updateContactConnectionStats(contact, stats) } } catch let error { logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))") let a = getErrorAlert(error, "Error synchronizing connection") await MainActor.run { alert = .error(title: a.title, error: a.message) } } } } private func syncAllowedAlert(_ syncConnection: @escaping () -> Void) -> Alert { Alert( title: Text("Fix connection?"), message: message(), primaryButton: .default(Text("Fix"), action: syncConnection), secondaryButton: .cancel() ) } } //struct CIRcvDecryptionError_Previews: PreviewProvider { // static var previews: some View { // CIRcvDecryptionError(msgDecryptError: .ratchetHeader, msgCount: 1, chatItem: ChatItem.getIntegrityErrorSample()) // } //}