diff --git a/Monal/Classes/AddContactMenu.swift b/Monal/Classes/AddContactMenu.swift index c573add..bb5399b 100644 --- a/Monal/Classes/AddContactMenu.swift +++ b/Monal/Classes/AddContactMenu.swift @@ -11,11 +11,11 @@ import UniformTypeIdentifiers struct AddContactMenu: View { var delegate: SheetDismisserProtocol - static private let jidFaultyPattern = "^([^@]+@)?.+(\\..{2,})?$" + private static let jidFaultyPattern = "^([^@]+@)?.+(\\..{2,})?$" @State private var enabledAccounts: [xmpp] @State private var selectedAccount: Int - @State private var scannedFingerprints: [NSNumber:Data]? = nil + @State private var scannedFingerprints: [NSNumber: Data]? = nil @State private var importScannedFingerprints: Bool = false @State private var toAdd: String = "" @@ -23,43 +23,43 @@ struct AddContactMenu: View { @State private var showAlert = false // note: dismissLabel is not accessed but defined at the .alert() section @State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close")) - @State private var invitationResult: [String:AnyObject]? = nil + @State private var invitationResult: [String: AnyObject]? = nil @StateObject private var overlay = LoadingOverlayState() @State private var showQRCodeScanner = false @State private var success = false - @State private var newContact : MLContact? + @State private var newContact: MLContact? @State private var isEditingJid = false - private let dismissWithNewContact: (MLContact) -> () + private let dismissWithNewContact: (MLContact) -> Void private let preauthToken: String? - init(delegate: SheetDismisserProtocol, dismissWithNewContact: @escaping (MLContact) -> (), prefillJid: String = "", preauthToken:String? = nil, prefillAccount:xmpp? = nil, omemoFingerprints: [NSNumber:Data]? = nil) { + init(delegate: SheetDismisserProtocol, dismissWithNewContact: @escaping (MLContact) -> Void, prefillJid: String = "", preauthToken: String? = nil, prefillAccount: xmpp? = nil, omemoFingerprints: [NSNumber: Data]? = nil) { self.delegate = delegate self.dismissWithNewContact = dismissWithNewContact - //self.toAdd = State(wrappedValue: prefillJid) - self.toAdd = prefillJid + // self.toAdd = State(wrappedValue: prefillJid) + toAdd = prefillJid self.preauthToken = preauthToken - //only display omemo ui part if there are any fingerprints (the checks below test for nil, not for 0) + // only display omemo ui part if there are any fingerprints (the checks below test for nil, not for 0) if omemoFingerprints?.count ?? 0 > 0 { - self.scannedFingerprints = omemoFingerprints + scannedFingerprints = omemoFingerprints } - + let enabledAccounts = MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp] self.enabledAccounts = enabledAccounts - self.selectedAccount = enabledAccounts.first != nil ? 0 : -1; + selectedAccount = enabledAccounts.first != nil ? 0 : -1 if let prefillAccount = prefillAccount { for index in enabledAccounts.indices { - if enabledAccounts[index].accountID.isEqual(to:prefillAccount.accountID) { - self.selectedAccount = index + if enabledAccounts[index].accountID.isEqual(to: prefillAccount.accountID) { + selectedAccount = index } } } } - - // FIXME duplicate code from WelcomeLogIn.swift, maybe move to SwiftuiHelpers + + // FIXME: duplicate code from WelcomeLogIn.swift, maybe move to SwiftuiHelpers private var toAddEmptyAlert: Bool { alertPrompt.title = Text("No Empty Values!") alertPrompt.message = Text("Please make sure you have entered a valid jid.") @@ -71,88 +71,88 @@ struct AddContactMenu: View { alertPrompt.message = Text("The jid you want to add should be in in the format user@domain.tld.") return toAddInvalid } - + private func errorAlert(title: Text, message: Text = Text("")) { alertPrompt.title = title alertPrompt.message = message showAlert = true } - + private func successAlert(title: Text, message: Text) { alertPrompt.title = title alertPrompt.message = message - self.success = true // < dismiss entire view on close + success = true // < dismiss entire view on close showAlert = true } - + private var toAddEmpty: Bool { - return toAdd.isEmpty - } - - private var toAddInvalid: Bool { - return toAdd.range(of: AddContactMenu.jidFaultyPattern, options:.regularExpression) == nil + toAdd.isEmpty } - func trustFingerprints(_ fingerprints:[NSNumber:Data]?, for jid:String, on account:xmpp) { - //we don't untrust other devices not included in here, because conversations only exports its own fingerprint + private var toAddInvalid: Bool { + toAdd.range(of: AddContactMenu.jidFaultyPattern, options: .regularExpression) == nil + } + + func trustFingerprints(_ fingerprints: [NSNumber: Data]?, for jid: String, on account: xmpp) { + // we don't untrust other devices not included in here, because conversations only exports its own fingerprint if let fingerprints = fingerprints { for (deviceId, fingerprint) in fingerprints { - let address = SignalAddress.init(name:jid, deviceId:deviceId.int32Value) - let knownDevices = Array(account.omemo.knownDevices(forAddressName:jid)) + let address = SignalAddress(name: jid, deviceId: deviceId.int32Value) + let knownDevices = Array(account.omemo.knownDevices(forAddressName: jid)) if !knownDevices.contains(deviceId) { - account.omemo.addIdentityManually(address, identityKey:fingerprint) + account.omemo.addIdentityManually(address, identityKey: fingerprint) assert(account.omemo.getIdentityFor(address) == fingerprint, "The stored and created fingerprint should match") } - //trust device/fingerprint if fingerprints match - let knownFingerprintHex = HelperTools.signalHexKey(with:account.omemo.getIdentityFor(address)) - let addedFingerprintHex = HelperTools.signalHexKey(with:fingerprint) + // trust device/fingerprint if fingerprints match + let knownFingerprintHex = HelperTools.signalHexKey(with: account.omemo.getIdentityFor(address)) + let addedFingerprintHex = HelperTools.signalHexKey(with: fingerprint) if knownFingerprintHex.uppercased() == addedFingerprintHex.uppercased() { - account.omemo.updateTrust(true, for:address) + account.omemo.updateTrust(true, for: address) } } } } - + func addJid(jid: String) { - let account = self.enabledAccounts[selectedAccount] + let account = enabledAccounts[selectedAccount] let contact = MLContact.createContact(fromJid: jid, andAccountID: account.accountID) if contact.isInRoster { - self.newContact = contact - //import omemo fingerprints as manually trusted, if requested - trustFingerprints(self.importScannedFingerprints ? self.scannedFingerprints : [:], for:jid, on:account) - //only alert of already known contact if we did not import the omemo fingerprints - if !self.importScannedFingerprints || self.scannedFingerprints?.count ?? 0 == 0 { - if self.enabledAccounts.count > 1 { - self.success = true + newContact = contact + // import omemo fingerprints as manually trusted, if requested + trustFingerprints(importScannedFingerprints ? scannedFingerprints : [:], for: jid, on: account) + // only alert of already known contact if we did not import the omemo fingerprints + if !importScannedFingerprints || scannedFingerprints?.count ?? 0 == 0 { + if enabledAccounts.count > 1 { + success = true successAlert(title: Text("Already present"), message: Text("This contact is already in the contact list of the selected account")) } else { - self.success = true + success = true successAlert(title: Text("Already present"), message: Text("This contact is already in your contact list")) } } return } - showPromisingLoadingOverlay(overlay, headline:NSLocalizedString("Adding...", comment: ""), description:"") { + showPromisingLoadingOverlay(overlay, headline: NSLocalizedString("Adding...", comment: ""), description: "") { account.checkJidType(jid) }.done { type in let type = type as! String if type == "account" { let contact = MLContact.createContact(fromJid: jid, andAccountID: account.accountID) self.newContact = contact - MLXMPPManager.sharedInstance().add(contact, withPreauthToken:preauthToken) - //import omemo fingerprints as manually trusted, if requested - trustFingerprints(self.importScannedFingerprints ? self.scannedFingerprints : [:], for:jid, on:account) + MLXMPPManager.sharedInstance().add(contact, withPreauthToken: preauthToken) + // import omemo fingerprints as manually trusted, if requested + trustFingerprints(self.importScannedFingerprints ? self.scannedFingerprints : [:], for: jid, on: account) successAlert(title: Text("Permission Requested"), message: Text("The new contact will be added to your contacts list when the person you've added has approved your request.")) } else if type == "muc" { - showPromisingLoadingOverlay(overlay, headlineView:Text("Adding Group/Channel..."), descriptionView:Text("")) { - promisifyMucAction(account:account, mucJid:jid) { + showPromisingLoadingOverlay(overlay, headlineView: Text("Adding Group/Channel..."), descriptionView: Text("")) { + promisifyMucAction(account: account, mucJid: jid) { account.joinMuc(jid) } }.done { _ in self.newContact = MLContact.createContact(fromJid: jid, andAccountID: account.accountID) successAlert(title: Text("Success!"), message: Text("Successfully joined group/channel \(jid)!")) }.catch { error in - errorAlert(title: Text("Error entering group/channel!"), message: Text("\(String(describing:error))")) + errorAlert(title: Text("Error entering group/channel!"), message: Text("\(String(describing: error))")) } } }.catch { error in @@ -161,20 +161,18 @@ struct AddContactMenu: View { } var body: some View { - let account = self.enabledAccounts[selectedAccount] + let account = enabledAccounts[selectedAccount] let splitJid = HelperTools.splitJid(account.connectionProperties.identity.jid) Form { if enabledAccounts.isEmpty { Text("Please make sure at least one account has connected before trying to add a contact or channel.") .foregroundColor(.secondary) - } - else - { - if DataLayer.sharedInstance().allContactRequests().count > 0 { + } else { + if !DataLayer.sharedInstance().allContactRequests().isEmpty { ContactRequestsMenu() } - - Section(header:Text("Contact and Group/Channel Jids are usually in the format: name@domain.tld")) { + + Section(header: Text("Contact and Group/Channel Jids are usually in the format: name@domain.tld")) { if enabledAccounts.count > 1 { Picker("Use account", selection: $selectedAccount) { ForEach(Array(self.enabledAccounts.enumerated()), id: \.element) { idx, account in @@ -189,19 +187,19 @@ struct AddContactMenu: View { .autocapitalization(.none) .autocorrectionDisabled() .keyboardType(.emailAddress) - .addClearButton(isEditing: isEditingJid, text:$toAdd) + .addClearButton(isEditing: isEditingJid, text: $toAdd) .disabled(scannedFingerprints != nil) .foregroundColor(scannedFingerprints != nil ? .secondary : .primary) .onChange(of: toAdd) { _ in toAdd = toAdd.replacingOccurrences(of: " ", with: "") } - - if scannedFingerprints != nil && scannedFingerprints!.count > 0 { + + if scannedFingerprints != nil && !scannedFingerprints!.isEmpty { Section(header: Text("A contact was scanned through the QR code scanner")) { Toggle(isOn: $importScannedFingerprints) { Text("Import and trust OMEMO fingerprints from QR code") } } } - + if scannedFingerprints != nil { Button(action: { toAdd = "" @@ -212,10 +210,10 @@ struct AddContactMenu: View { .foregroundColor(.red) }) } - + HStack { Spacer() - + Button(action: { showAlert = toAddEmptyAlert || toAddInvalidAlert @@ -236,8 +234,8 @@ struct AddContactMenu: View { .buttonStyle(MonalProminentButtonStyle()) } } - - if DataLayer.sharedInstance().allContactRequests().count == 0 { + + if DataLayer.sharedInstance().allContactRequests().isEmpty { Section { ContactRequestsMenu() } @@ -246,7 +244,7 @@ struct AddContactMenu: View { } .padding() .alert(isPresented: $showAlert) { - Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: { + Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(Text("Close"), action: { showAlert = false if self.success == true { if self.newContact != nil { @@ -257,7 +255,7 @@ struct AddContactMenu: View { } })) } - .richAlert(isPresented: $invitationResult, title:Text("Invitation for \(splitJid["host"]!) created")) { data in + .richAlert(isPresented: $invitationResult, title: Text("Invitation for \(splitJid["host"]!) created")) { data in VStack { Image(uiImage: createQrCode(value: data["landing"] as! String)) .interpolation(.none) @@ -266,15 +264,15 @@ struct AddContactMenu: View { .aspectRatio(1, contentMode: .fit) if let expires = data["expires"] as? Date { - Text("This invitation will expire on \(expires.formatted(date:.numeric, time:.shortened))") - .font(.footnote) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, alignment: .leading) + Text("This invitation will expire on \(expires.formatted(date: .numeric, time: .shortened))") + .font(.footnote) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) } } - } buttons: { data in + } buttons: { data in Button(action: { - UIPasteboard.general.setValue(data["landing"] as! String, forPasteboardType:UTType.utf8PlainText.identifier as String) + UIPasteboard.general.setValue(data["landing"] as! String, forPasteboardType: UTType.utf8PlainText.identifier as String) invitationResult = nil }) { ShareLink("Share invitation link", item: URL(string: data["landing"] as! String)!) @@ -309,17 +307,17 @@ struct AddContactMenu: View { ToolbarItemGroup(placement: .navigationBarTrailing) { if account.connectionProperties.discoveredAdhocCommands["urn:xmpp:invite#invite"] != nil { Button(action: { - DDLogVerbose("Trying to create invitation for: \(String(describing:splitJid["host"]!))") + DDLogVerbose("Trying to create invitation for: \(String(describing: splitJid["host"]!))") showLoadingOverlay(overlay, headline: NSLocalizedString("Creating invitation...", comment: "")) account.createInvitation(completion: { - let result = $0 as! [String:AnyObject] + let result = $0 as! [String: AnyObject] DispatchQueue.main.async { hideLoadingOverlay(overlay) - DDLogVerbose("Got invitation result: \(String(describing:result))") + DDLogVerbose("Got invitation result: \(String(describing: result))") if result["success"] as! Bool == true { invitationResult = result } else { - errorAlert(title:Text("Failed to create invitation for \(splitJid["host"]!)"), message:Text(result["error"] as! String)) + errorAlert(title: Text("Failed to create invitation for \(splitJid["host"]!)"), message: Text(result["error"] as! String)) } } }) @@ -341,7 +339,7 @@ struct AddContactMenu: View { struct AddContactMenu_Previews: PreviewProvider { static var delegate = SheetDismisserProtocol() static var previews: some View { - AddContactMenu(delegate: delegate, dismissWithNewContact: { c in + AddContactMenu(delegate: delegate, dismissWithNewContact: { _ in }) } } diff --git a/Monal/Classes/ContactsView.swift b/Monal/Classes/ContactsView.swift index 5e2f037..2c89332 100644 --- a/Monal/Classes/ContactsView.swift +++ b/Monal/Classes/ContactsView.swift @@ -11,12 +11,12 @@ import SwiftUI struct ContactViewEntry: View { private let contact: MLContact @Binding private var selectedContactForContactDetails: ObservableKVOWrapper? - private let dismissWithContact: (MLContact) -> () + private let dismissWithContact: (MLContact) -> Void @State private var shouldPresentRemoveContactAlert: Bool = false private var removeContactButtonText: String { - if (!isDeletable) { + if !isDeletable { return "Cannot delete notes to self" } return contact.isMuc ? "Remove Conversation" : "Remove Contact" @@ -34,9 +34,9 @@ struct ContactViewEntry: View { !contact.isSelfChat } - init (contact: MLContact, selectedContactForContactDetails: Binding?>, dismissWithContact: @escaping (MLContact) -> ()) { + init(contact: MLContact, selectedContactForContactDetails: Binding?>, dismissWithContact: @escaping (MLContact) -> Void) { self.contact = contact - self._selectedContactForContactDetails = selectedContactForContactDetails + _selectedContactForContactDetails = selectedContactForContactDetails self.dismissWithContact = dismissWithContact } @@ -88,26 +88,26 @@ struct ContactViewEntry: View { struct ContactsView: View { @ObservedObject private var contacts: Contacts private let delegate: SheetDismisserProtocol - private let dismissWithContact: (MLContact) -> () + private let dismissWithContact: (MLContact) -> Void @State private var searchText: String = "" @State private var selectedContactForContactDetails: ObservableKVOWrapper? = nil - init(contacts: Contacts, delegate: SheetDismisserProtocol, dismissWithContact: @escaping (MLContact) -> ()) { + init(contacts: Contacts, delegate: SheetDismisserProtocol, dismissWithContact: @escaping (MLContact) -> Void) { self.contacts = contacts self.delegate = delegate self.dismissWithContact = dismissWithContact } private static func shouldDisplayContact(contact: MLContact) -> Bool { -#if IS_QUICKSY - return true -#endif + #if IS_QUICKSY + return true + #endif return contact.isSubscribedTo || contact.hasOutgoingContactRequest || contact.isSubscribedFrom } private var contactList: [MLContact] { - return contacts.contacts + contacts.contacts .filter(ContactsView.shouldDisplayContact) .sorted { ContactsView.sortingCriteria($0) < ContactsView.sortingCriteria($1) } } @@ -118,7 +118,7 @@ struct ContactsView: View { } private static func sortingCriteria(_ contact: MLContact) -> (String, String) { - return (contact.contactDisplayName.lowercased(), contact.contactJid.lowercased()) + (contact.contactDisplayName.lowercased(), contact.contactJid.lowercased()) } private func searchMatchesContact(contact: MLContact, search: String) -> Bool { @@ -164,7 +164,7 @@ struct ContactsView: View { } } .sheet(item: $selectedContactForContactDetails) { selectedContact in - AnyView(AddTopLevelNavigation(withDelegate: delegate, to: ContactDetails(delegate:delegate, contact:selectedContact))) + AnyView(AddTopLevelNavigation(withDelegate: delegate, to: ContactDetails(delegate: delegate, contact: selectedContact))) } } } @@ -175,20 +175,20 @@ class Contacts: ObservableObject { private var subscriptions: Set = Set() init() { - self.contacts = Set(DataLayer.sharedInstance().contactList()) - self.requestCount = DataLayer.sharedInstance().allContactRequests().count + contacts = Set(DataLayer.sharedInstance().contactList()) + requestCount = DataLayer.sharedInstance().allContactRequests().count subscriptions = [ NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRemoved")) .receive(on: DispatchQueue.main) - .sink() { _ in self.refreshContacts() }, + .sink { _ in self.refreshContacts() }, NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRefresh")) .receive(on: DispatchQueue.main) - .sink() { _ in self.refreshContacts() } + .sink { _ in self.refreshContacts() } ] } private func refreshContacts() { - self.contacts = Set(DataLayer.sharedInstance().contactList()) - self.requestCount = DataLayer.sharedInstance().allContactRequests().count + contacts = Set(DataLayer.sharedInstance().contactList()) + requestCount = DataLayer.sharedInstance().allContactRequests().count } } diff --git a/Monal/another.im/Views/Main/Contacts/AddContactOrChannelScreen.swift b/Monal/another.im/Views/Main/Contacts/AddContactOrChannelScreen.swift index ed76069..5985338 100644 --- a/Monal/another.im/Views/Main/Contacts/AddContactOrChannelScreen.swift +++ b/Monal/another.im/Views/Main/Contacts/AddContactOrChannelScreen.swift @@ -123,17 +123,7 @@ struct AddContactOrChannelScreen: View { router.dismissModal() } - do { - try await wrapper.addContact(contactJid: contactJID, forAccountID: ownerAccount.id) - router.dismissScreen() - } catch { - router.showAlert( - .alert, - title: L10n.Global.Error.title, - subtitle: L10n.Contacts.Add.serverError - ) { - Button(L10n.Global.ok, role: .cancel) {} - } - } + await wrapper.addContact(contactJid: contactJID, forAccountID: ownerAccount.id) + router.dismissScreen() } } diff --git a/Monal/another.im/Views/Main/Contacts/ContactsScreen.swift b/Monal/another.im/Views/Main/Contacts/ContactsScreen.swift index 8dd4c53..1431ae9 100644 --- a/Monal/another.im/Views/Main/Contacts/ContactsScreen.swift +++ b/Monal/another.im/Views/Main/Contacts/ContactsScreen.swift @@ -50,7 +50,7 @@ private struct ContactsScreenRow: View { var body: some View { SharedListRow( - iconType: .charCircle(contact.name?.firstLetter ?? contact.contactJid.firstLetter), + iconType: .charCircle(contact.name ?? contact.contactJid), text: contact.contactJid, controlType: .none ) diff --git a/Monal/another.im/XMPP/AimErrors.swift b/Monal/another.im/XMPP/AimErrors.swift index dbd5786..dd621d5 100644 --- a/Monal/another.im/XMPP/AimErrors.swift +++ b/Monal/another.im/XMPP/AimErrors.swift @@ -1,4 +1,3 @@ enum AimErrors: Error { case loginError - case addContactError } diff --git a/Monal/another.im/XMPP/MonalWrapperModels.swift b/Monal/another.im/XMPP/MonalWrapperModels.swift index fa5fab3..7ec89b0 100644 --- a/Monal/another.im/XMPP/MonalWrapperModels.swift +++ b/Monal/another.im/XMPP/MonalWrapperModels.swift @@ -51,6 +51,6 @@ struct Contact: Identifiable { init?(_ obj: MLContact) { ownerId = obj.accountID.intValue contactJid = obj.contactJid - name = obj.nickName + name = obj.nickName.isEmpty ? nil : obj.nickName } } diff --git a/Monal/another.im/XMPP/MonalXmppWrapper.swift b/Monal/another.im/XMPP/MonalXmppWrapper.swift index 3de914f..6da45a3 100644 --- a/Monal/another.im/XMPP/MonalXmppWrapper.swift +++ b/Monal/another.im/XMPP/MonalXmppWrapper.swift @@ -41,15 +41,13 @@ extension MonalXmppWrapper { } } - func addContact(contactJid: String, forAccountID: Int) async throws { - _ = await Task { [weak self] in - let result = self?.db.addContact(contactJid, forAccount: NSNumber(value: forAccountID), nickname: nil) ?? true - if result { - NotificationCenter.default.post(name: Notification.Name(kMonalContactRefresh), object: nil) - } else { - throw AimErrors.addContactError - } - }.result + func addContact(contactJid: String, forAccountID: Int) async { + await withCheckedContinuation { [weak self] cnt in + let contact = MLContact.createContact(fromJid: contactJid, andAccountID: NSNumber(value: forAccountID)) + self?.xmpp.add(contact) + cnt.resume() + } + NotificationCenter.default.post(name: Notification.Name(kMonalContactRefresh), object: nil) } } @@ -137,6 +135,9 @@ private extension MonalXmppWrapper { } func refreshContacts() { - contacts = db.contactList().compactMap { Contact($0) } + contacts = db.contactList() + .filter { $0.isSubscribedTo || $0.hasOutgoingContactRequest || $0.isSubscribedFrom } + .filter { !$0.isSelfChat } // removed for now + .compactMap { Contact($0) } } }