diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index f10b9228a..cce7896aa 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -55,9 +55,15 @@ 32DCAF9DA9EC47798DF8BB73 /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA9701B2D5052225A0414 /* SignInViewController.swift */; }; 50531BE42629B9A80039BAE9 /* AttachmentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50531BE32629B9A80039BAE9 /* AttachmentNode.swift */; }; 512C1414271077F8002DE13F /* GoogleAPIClientForREST_PeopleService in Frameworks */ = {isa = PBXBuildFile; productRef = 512C1413271077F8002DE13F /* GoogleAPIClientForREST_PeopleService */; }; + 5133B6702716320F00C95463 /* ContactKeyDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5133B66F2716320F00C95463 /* ContactKeyDetailViewController.swift */; }; + 5133B6722716321F00C95463 /* ContactKeyDetailDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5133B6712716321F00C95463 /* ContactKeyDetailDecorator.swift */; }; + 5133B6742716E5EA00C95463 /* LabelCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5133B6732716E5EA00C95463 /* LabelCellNode.swift */; }; 51775C32270B01C200D7C944 /* PrvKeyInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51775C31270B01C200D7C944 /* PrvKeyInfoTests.swift */; }; 51775C39270C7D2400D7C944 /* StorageMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51775C38270C7D2400D7C944 /* StorageMethod.swift */; }; 51B4AE4F27137CB70001F33B /* Promises in Frameworks */ = {isa = PBXBuildFile; productRef = 51B4AE4E27137CB70001F33B /* Promises */; }; + 51B4AE51271444580001F33B /* ContactKeyObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B4AE50271444580001F33B /* ContactKeyObject.swift */; }; + 51B4AE5327144E590001F33B /* PubKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B4AE5227144E590001F33B /* PubKey.swift */; }; + 51DE2FEE2714DA0400916222 /* ContactKeyCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DE2FED2714DA0400916222 /* ContactKeyCellNode.swift */; }; 51C0C1EF271982A1000C9738 /* MailCore in Frameworks */ = {isa = PBXBuildFile; productRef = 51C0C1EE271982A1000C9738 /* MailCore */; }; 51C0C1F2271987DB000C9738 /* Toast in Frameworks */ = {isa = PBXBuildFile; productRef = 51C0C1F1271987DB000C9738 /* Toast */; }; 51E13F12270F92BA00F287CA /* IDZSwiftCommonCrypto in Frameworks */ = {isa = PBXBuildFile; productRef = 51E13F11270F92BA00F287CA /* IDZSwiftCommonCrypto */; }; @@ -284,7 +290,7 @@ D274724424FD1932006BA6EF /* FolderObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = D274724324FD1932006BA6EF /* FolderObject.swift */; }; D27B911924EFE79F002DF0A1 /* LocalContactsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27B911824EFE79F002DF0A1 /* LocalContactsProvider.swift */; }; D27B911D24EFE806002DF0A1 /* ContactObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27B911C24EFE806002DF0A1 /* ContactObject.swift */; }; - D27B911F24EFE828002DF0A1 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27B911E24EFE828002DF0A1 /* Contact.swift */; }; + D27B911F24EFE828002DF0A1 /* RecipientWithPubKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27B911E24EFE828002DF0A1 /* RecipientWithPubKeys.swift */; }; D28325DD24895F5700D311BD /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 949ED9412303E3B400530579 /* Colors.xcassets */; }; D28655912423B4580066F52E /* HeaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDF3639235A0B3B00614596 /* HeaderNode.swift */; }; D28655932423B4EE0066F52E /* MyMenuViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28655922423B4EE0066F52E /* MyMenuViewDecorator.swift */; }; @@ -328,7 +334,7 @@ D2E26F6824F169E300612AF1 /* ContactCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E26F6724F169E300612AF1 /* ContactCellNode.swift */; }; D2E26F6C24F25B1F00612AF1 /* KeyAlgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E26F6B24F25B1F00612AF1 /* KeyAlgo.swift */; }; D2E26F7024F266F300612AF1 /* ContactDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E26F6F24F266F300612AF1 /* ContactDetailViewController.swift */; }; - D2E26F7224F26FFF00612AF1 /* ContactDetailNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E26F7124F26FFF00612AF1 /* ContactDetailNode.swift */; }; + D2E26F7224F26FFF00612AF1 /* ContactUserCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E26F7124F26FFF00612AF1 /* ContactUserCellNode.swift */; }; D2E26F7424F2705B00612AF1 /* ContactDetailDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2E26F7324F2705B00612AF1 /* ContactDetailDecorator.swift */; }; D2F18464244B0C63000CC5D1 /* SignInGoogleTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F18463244B0C63000CC5D1 /* SignInGoogleTest.swift */; }; D2F18468244B0F35000CC5D1 /* AppTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2F18467244B0F35000CC5D1 /* AppTestHelper.swift */; }; @@ -467,8 +473,14 @@ 4C5032E4FC5685A224F61785 /* Pods-FlowCryptUI.testflight.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptUI.testflight.xcconfig"; path = "Target Support Files/Pods-FlowCryptUI/Pods-FlowCryptUI.testflight.xcconfig"; sourceTree = ""; }; 4F928D493732294B4E521900 /* Pods-FlowCryptUIApplication.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptUIApplication.release.xcconfig"; path = "Target Support Files/Pods-FlowCryptUIApplication/Pods-FlowCryptUIApplication.release.xcconfig"; sourceTree = ""; }; 50531BE32629B9A80039BAE9 /* AttachmentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentNode.swift; sourceTree = ""; }; + 5133B66F2716320F00C95463 /* ContactKeyDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactKeyDetailViewController.swift; sourceTree = ""; }; + 5133B6712716321F00C95463 /* ContactKeyDetailDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactKeyDetailDecorator.swift; sourceTree = ""; }; + 5133B6732716E5EA00C95463 /* LabelCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelCellNode.swift; sourceTree = ""; }; 51775C31270B01C200D7C944 /* PrvKeyInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrvKeyInfoTests.swift; sourceTree = ""; }; 51775C38270C7D2400D7C944 /* StorageMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageMethod.swift; sourceTree = ""; }; + 51B4AE50271444580001F33B /* ContactKeyObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactKeyObject.swift; sourceTree = ""; }; + 51B4AE5227144E590001F33B /* PubKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubKey.swift; sourceTree = ""; }; + 51DE2FED2714DA0400916222 /* ContactKeyCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactKeyCellNode.swift; sourceTree = ""; }; 51E1673C270DAFF900D27C52 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; }; 55652F68438D6EDFE71EA13C /* Pods-FlowCryptUIApplication.enterprise.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptUIApplication.enterprise.xcconfig"; path = "Target Support Files/Pods-FlowCryptUIApplication/Pods-FlowCryptUIApplication.enterprise.xcconfig"; sourceTree = ""; }; 5A39F42C239EC321001F4607 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; @@ -704,7 +716,7 @@ D274724324FD1932006BA6EF /* FolderObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderObject.swift; sourceTree = ""; }; D27B911824EFE79F002DF0A1 /* LocalContactsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalContactsProvider.swift; sourceTree = ""; }; D27B911C24EFE806002DF0A1 /* ContactObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactObject.swift; sourceTree = ""; }; - D27B911E24EFE828002DF0A1 /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = ""; }; + D27B911E24EFE828002DF0A1 /* RecipientWithPubKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientWithPubKeys.swift; sourceTree = ""; }; D28655922423B4EE0066F52E /* MyMenuViewDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyMenuViewDecorator.swift; sourceTree = ""; }; D28655942423BFF60066F52E /* SideMenuOptionalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuOptionalView.swift; sourceTree = ""; }; D2891AC124C59EFA008918E3 /* KeyService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyService.swift; sourceTree = ""; }; @@ -727,7 +739,7 @@ D2E26F6724F169E300612AF1 /* ContactCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactCellNode.swift; sourceTree = ""; }; D2E26F6B24F25B1F00612AF1 /* KeyAlgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyAlgo.swift; sourceTree = ""; }; D2E26F6F24F266F300612AF1 /* ContactDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDetailViewController.swift; sourceTree = ""; }; - D2E26F7124F26FFF00612AF1 /* ContactDetailNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDetailNode.swift; sourceTree = ""; }; + D2E26F7124F26FFF00612AF1 /* ContactUserCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUserCellNode.swift; sourceTree = ""; }; D2E26F7324F2705B00612AF1 /* ContactDetailDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDetailDecorator.swift; sourceTree = ""; }; D2F18463244B0C63000CC5D1 /* SignInGoogleTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInGoogleTest.swift; sourceTree = ""; }; D2F18467244B0F35000CC5D1 /* AppTestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTestHelper.swift; sourceTree = ""; }; @@ -1092,6 +1104,7 @@ children = ( D2F41372243CC7990066AFB5 /* UserObject.swift */, D27B911C24EFE806002DF0A1 /* ContactObject.swift */, + 51B4AE50271444580001F33B /* ContactKeyObject.swift */, D274724324FD1932006BA6EF /* FolderObject.swift */, 04B4728B1ECE29D200B8266F /* KeyInfo.swift */, D2F41370243CC76E0066AFB5 /* SessionObject.swift */, @@ -1803,7 +1816,8 @@ D27B912024EFE842002DF0A1 /* Models */ = { isa = PBXGroup; children = ( - D27B911E24EFE828002DF0A1 /* Contact.swift */, + D27B911E24EFE828002DF0A1 /* RecipientWithPubKeys.swift */, + 51B4AE5227144E590001F33B /* PubKey.swift */, ); path = Models; sourceTree = ""; @@ -1916,9 +1930,11 @@ 9FDF3637235A0B3100614596 /* InfoCellNode.swift */, D2F6D12E24324ACC00DB4065 /* SwitchCellNode.swift */, D2E26F6724F169E300612AF1 /* ContactCellNode.swift */, - D2E26F7124F26FFF00612AF1 /* ContactDetailNode.swift */, + D2E26F7124F26FFF00612AF1 /* ContactUserCellNode.swift */, + 51DE2FED2714DA0400916222 /* ContactKeyCellNode.swift */, D20D3C7B2520ABC600D4AA9A /* BackupCellNode.swift */, D28A1CBC2525C141003B760B /* CheckBoxTextNode.swift */, + 5133B6732716E5EA00C95463 /* LabelCellNode.swift */, ); path = "Cell Nodes"; sourceTree = ""; @@ -1959,6 +1975,8 @@ D2E26F6524F169B400612AF1 /* ContactsListDecorator.swift */, D2E26F6F24F266F300612AF1 /* ContactDetailViewController.swift */, D2E26F7324F2705B00612AF1 /* ContactDetailDecorator.swift */, + 5133B66F2716320F00C95463 /* ContactKeyDetailViewController.swift */, + 5133B6712716321F00C95463 /* ContactKeyDetailDecorator.swift */, ); path = "Contacts List"; sourceTree = ""; @@ -2492,6 +2510,7 @@ 9FB22CE425715D3E0026EE64 /* GmailServiceErrorHandler.swift in Sources */, 9F4163E6266520B600106194 /* CommonNodesInputs.swift in Sources */, 04B472951ECE29F600B8266F /* MyMenuViewController.swift in Sources */, + 51B4AE5327144E590001F33B /* PubKey.swift in Sources */, 21623D1826FA860700A11B9A /* PhotosManager.swift in Sources */, C132B9B41EC2DBD800763715 /* AppDelegate.swift in Sources */, 21489B7C267CBA0E00BDE4AC /* ClientConfiguration.swift in Sources */, @@ -2553,6 +2572,7 @@ 9FB22CDD25715CF50026EE64 /* GmailServiceError.swift in Sources */, 5ADEDCB923A42B9400EC495E /* KeyDetailViewDecorator.swift in Sources */, 9F416428266575DC00106194 /* BackupServiceType.swift in Sources */, + 5133B6702716320F00C95463 /* ContactKeyDetailViewController.swift in Sources */, 9F7E5137267AA51B00CE37C3 /* AlertsFactory.swift in Sources */, 5A39F437239ECC23001F4607 /* KeySettingsViewController.swift in Sources */, 9FF0673325520DE400FCC9E6 /* GmailService+send.swift in Sources */, @@ -2577,7 +2597,7 @@ 9F3EF33123B1785600FA0CEF /* MsgListViewConroller.swift in Sources */, 9F31ABA0232C071700CF87EA /* GlobalRouter.swift in Sources */, 9F5C2A92257E94DF00DE9B4B /* Imap+MessageOperations.swift in Sources */, - D27B911F24EFE828002DF0A1 /* Contact.swift in Sources */, + D27B911F24EFE828002DF0A1 /* RecipientWithPubKeys.swift in Sources */, 32DCA1414EEA727B86C337D5 /* Core.swift in Sources */, 9FB22CF725715DC50026EE64 /* KeyServiceErrorHandler.swift in Sources */, 9F0C3C1A231819C500299985 /* MessageKindProviderType.swift in Sources */, @@ -2589,6 +2609,7 @@ 51775C39270C7D2400D7C944 /* StorageMethod.swift in Sources */, 9FC4112E2595EA8B001180A8 /* Gmail+Search.swift in Sources */, 5A948DC5239EF2F4006284D7 /* LegalViewController.swift in Sources */, + 5133B6722716321F00C95463 /* ContactKeyDetailDecorator.swift in Sources */, D274724124F97C5C006BA6EF /* CacheService.swift in Sources */, A3B7C31923F576BA0022D628 /* AppStartup.swift in Sources */, 9F31AB8E23298BCF00CF87EA /* Imap+folders.swift in Sources */, @@ -2625,6 +2646,7 @@ 9FF0671025520D7100FCC9E6 /* MessageGateway.swift in Sources */, 9F31AB8C23298B3F00CF87EA /* Imap+retry.swift in Sources */, 9F79229426696B9300DA3D80 /* KeyDataStorage.swift in Sources */, + 51B4AE51271444580001F33B /* ContactKeyObject.swift in Sources */, 9F82D352256D74FA0069A702 /* InboxViewContainerController.swift in Sources */, D227C0E3250538100070F805 /* LocalFoldersProvider.swift in Sources */, 9FA405C7265AEBA50084D133 /* SetupGenerateKeyViewController.swift in Sources */, @@ -2684,9 +2706,11 @@ D271774A242558DA00BDA9A9 /* MessageSenderNode.swift in Sources */, D2717754242568A600BDA9A9 /* NavigationBarActionButton.swift in Sources */, D2A9CA3A2426198600E1D898 /* SignInDescriptionNode.swift in Sources */, + 5133B6742716E5EA00C95463 /* LabelCellNode.swift in Sources */, D211CE7123FC35AC00D1CE38 /* TextFieldCellNode.swift in Sources */, 9FA19890253C841F008C9CF2 /* TableViewController.swift in Sources */, D2CCD1FA247C50DA00D21F9C /* UIColorExtension.swift in Sources */, + 51DE2FEE2714DA0400916222 /* ContactKeyCellNode.swift in Sources */, D2A9CA432426210200E1D898 /* SetupTitleNode.swift in Sources */, D2F6D12F24324ACC00DB4065 /* SwitchCellNode.swift in Sources */, D2E26F6824F169E300612AF1 /* ContactCellNode.swift in Sources */, @@ -2695,7 +2719,7 @@ D2CDC3D72404704D002B045F /* RecipientEmailsCellNode.swift in Sources */, D2717752242567EB00BDA9A9 /* KeyTextCellNode.swift in Sources */, D211CE7B23FC59ED00D1CE38 /* InfoCellNode.swift in Sources */, - D2E26F7224F26FFF00612AF1 /* ContactDetailNode.swift in Sources */, + D2E26F7224F26FFF00612AF1 /* ContactUserCellNode.swift in Sources */, D211CE6F23FC358000D1CE38 /* ButtonNode.swift in Sources */, D28A1CBD2525C141003B760B /* CheckBoxTextNode.swift in Sources */, D2A9CA3B242619A400E1D898 /* SignInImageNode.swift in Sources */, @@ -3418,7 +3442,7 @@ ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailDecorator.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailDecorator.swift index c449ebaff..37e351bad 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailDecorator.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailDecorator.swift @@ -11,30 +11,40 @@ import Foundation protocol ContactDetailDecoratorType { var title: String { get } - func nodeInput(with contact: Contact) -> ContactDetailNode.Input + func userNodeInput(with contact: RecipientWithPubKeys) -> ContactUserCellNode.Input + func keyNodeInput(with key: PubKey) -> ContactKeyCellNode.Input } struct ContactDetailDecorator: ContactDetailDecoratorType { let title = "contact_detail_screen_title".localized - func nodeInput(with contact: Contact) -> ContactDetailNode.Input { + func userNodeInput(with contact: RecipientWithPubKeys) -> ContactUserCellNode.Input { + ContactUserCellNode.Input( + user: (contact.name ?? contact.email).attributed(.regular(16)) + ) + } + + func keyNodeInput(with key: PubKey) -> ContactKeyCellNode.Input { + let df = DateFormatter() + df.dateStyle = .medium + df.timeStyle = .medium + + let fingerpringString = key.fingerprint ?? "-" + let createdString: String = { - if let created = contact.pubkeyCreated { - let df = DateFormatter() - df.dateStyle = .medium - df.timeStyle = .medium - return df.string(from: created) - } else { - return "-" - } + guard let created = key.created else { return "-" } + return df.string(from: created) + }() + let expiresString: String = { + guard let expires = key.expiresOn else { return "never" } + return df.string(from: expires) }() - return ContactDetailNode.Input( - user: (contact.name ?? contact.email).attributed(.regular(16)), - ids: contact.longids.joined(separator: ",\n").attributed(.regular(14)), - fingerprints: contact.fingerprints.joined(separator: ",\n").attributed(.regular(14)), - algoInfo: contact.algo?.algorithm.attributed(.regular(14)), - created: createdString.attributed(.regular(14)) + + return ContactKeyCellNode.Input( + fingerprint: fingerpringString.attributed(.regular(13)), + createdAt: createdString.attributed(.regular(14)), + expires: expiresString.attributed(.regular(14)) ) } } diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailViewController.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailViewController.swift index ce67c1c7f..b57de248e 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailViewController.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailViewController.swift @@ -7,30 +7,38 @@ // import AsyncDisplayKit +import FlowCryptCommon import FlowCryptUI /** - * View controller which shows details about a contact and the public key recorded for it + * View controller which shows details about a contact and lists public keys recorded for it * - User can be redirected here from settings *ContactsListViewController* by tapping on a particular contact */ final class ContactDetailViewController: TableNodeViewController { typealias ContactDetailAction = (Action) -> Void enum Action { - case delete(_ contact: Contact) + case delete(_ recipient: RecipientWithPubKeys) + } + + private enum Section: Int, CaseIterable { + case header = 0, keys } private let decorator: ContactDetailDecoratorType - private let contact: Contact + private let contactsProvider: LocalContactsProviderType + private var recipient: RecipientWithPubKeys private let action: ContactDetailAction? init( decorator: ContactDetailDecoratorType = ContactDetailDecorator(), - contact: Contact, + contactsProvider: LocalContactsProviderType = LocalContactsProvider(), + recipient: RecipientWithPubKeys, action: ContactDetailAction? ) { self.decorator = decorator - self.contact = contact + self.contactsProvider = contactsProvider + self.recipient = recipient self.action = action super.init(node: TableNode()) } @@ -51,45 +59,97 @@ final class ContactDetailViewController: TableNodeViewController { private func setupNavigationBarItems() { navigationItem.rightBarButtonItem = NavigationBarItemsView( with: [ - .init(image: UIImage(named: "share"), action: (self, #selector(handleSaveAction))), - .init(image: UIImage(named: "copy"), action: (self, #selector(handleCopyAction))), - .init(image: UIImage(named: "trash"), action: (self, #selector(handleRemoveAction))) + .init(image: UIImage(systemName: "trash"), action: (self, #selector(handleRemoveAction))) ] ) } } extension ContactDetailViewController { - @objc private final func handleSaveAction() { - let vc = UIActivityViewController( - activityItems: [contact.pubKey], - applicationActivities: nil - ) - present(vc, animated: true, completion: nil) - } - - @objc private final func handleCopyAction() { - UIPasteboard.general.string = contact.pubKey - showToast("contact_detail_copy".localized) - } - @objc private final func handleRemoveAction() { navigationController?.popViewController(animated: true) { [weak self] in guard let self = self else { return } - self.action?(.delete(self.contact)) + self.action?(.delete(self.recipient)) } } + + private func delete(with context: Either) { + let keyToRemove: PubKey + let indexPathToRemove: IndexPath + switch context { + case .left(let key): + keyToRemove = key + guard let index = recipient.pubKeys.firstIndex(where: { $0 == key }) else { + assertionFailure("Can't find index of the contact") + return + } + indexPathToRemove = IndexPath(row: index, section: 1) + case .right(let indexPath): + indexPathToRemove = indexPath + keyToRemove = recipient.pubKeys[indexPath.row] + } + + recipient.remove(pubKey: keyToRemove) + if let fingerprint = keyToRemove.fingerprint, fingerprint.isNotEmpty { + contactsProvider.removePubKey(with: fingerprint, for: recipient.email) + } + node.deleteRows(at: [indexPathToRemove], with: .left) + } } extension ContactDetailViewController: ASTableDelegate, ASTableDataSource { - func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { - 1 + func numberOfSections(in tableNode: ASTableNode) -> Int { + Section.allCases.count + } + + func tableNode(_: ASTableNode, numberOfRowsInSection section: Int) -> Int { + guard let section = Section(rawValue: section) else { return 0 } + + switch section { + case .header: return 1 + case .keys: return recipient.pubKeys.count + } } func tableNode(_: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { { [weak self] in - guard let self = self else { return ASCellNode() } - return ContactDetailNode(input: self.decorator.nodeInput(with: self.contact)) + guard let self = self, let section = Section(rawValue: indexPath.section) + else { return ASCellNode() } + return self.node(for: section, row: indexPath.row) + } + } + + func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { + guard let section = Section(rawValue: indexPath.section) else { return } + + switch section { + case .header: + return + case .keys: + let pubKey = recipient.pubKeys[indexPath.row] + let contactKeyDetailViewController = ContactKeyDetailViewController(pubKey: pubKey) { [weak self] action in + guard case let .delete(key) = action else { + assertionFailure("Action is not implemented") + return + } + self?.delete(with: .left(key)) + } + + navigationController?.pushViewController(contactKeyDetailViewController, animated: true) + } + } +} + +// MARK: - UI +extension ContactDetailViewController { + private func node(for section: Section, row: Int) -> ASCellNode { + switch section { + case .header: + return ContactUserCellNode(input: self.decorator.userNodeInput(with: self.recipient)) + case .keys: + return ContactKeyCellNode( + input: self.decorator.keyNodeInput(with: self.recipient.pubKeys[row]) + ) } } } diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift new file mode 100644 index 000000000..0823b6801 --- /dev/null +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailDecorator.swift @@ -0,0 +1,35 @@ +// +// ContactKeyDetailDecorator.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 13/10/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import FlowCryptUI +import Foundation + +protocol ContactKeyDetailDecoratorType { + var title: String { get } + func attributedTitle(for contactKeyPart: ContactKeyDetailViewController.Part) -> NSAttributedString +} + +struct ContactKeyDetailDecorator: ContactKeyDetailDecoratorType { + let title = "contact_key_detail_screen_title".localized + + func attributedTitle(for contactKeyPart: ContactKeyDetailViewController.Part) -> NSAttributedString { + let title: String + switch contactKeyPart { + case .key: title = "contact_key_pub".localized + case .signature: title = "contact_key_signature".localized + case .created: title = "contact_key_created".localized + case .checked: title = "contact_key_fetched".localized + case .expire: title = "contact_key_expires".localized + case .longids: title = "contact_key_longids".localized + case .fingerprints: title = "contact_key_fingerprints".localized + case .algo: title = "contact_key_algo".localized + } + + return title.attributed(.bold(16)) + } +} diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailViewController.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailViewController.swift new file mode 100644 index 000000000..38af05f98 --- /dev/null +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactKeyDetailViewController.swift @@ -0,0 +1,139 @@ +// +// ContactKeyDetailViewController.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 13/10/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit +import FlowCryptCommon +import FlowCryptUI + +/** + * View controller which shows details about contact's public key + * - User can be redirected here from *ContactDetailViewController* by tapping on a particular key + */ +final class ContactKeyDetailViewController: TableNodeViewController { + typealias ContactKeyDetailAction = (Action) -> Void + + enum Action { + case delete(_ key: PubKey) + } + + enum Part: Int, CaseIterable { + case key = 0, signature, created, checked, expire, longids, fingerprints, algo + } + + private let decorator: ContactKeyDetailDecoratorType + private let pubKey: PubKey + private let action: ContactKeyDetailAction? + + init( + decorator: ContactKeyDetailDecoratorType = ContactKeyDetailDecorator(), + pubKey: PubKey, + action: ContactKeyDetailAction? + ) { + self.decorator = decorator + self.pubKey = pubKey + self.action = action + super.init(node: TableNode()) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + node.delegate = self + node.dataSource = self + title = decorator.title + setupNavigationBarItems() + } + + private func setupNavigationBarItems() { + navigationItem.rightBarButtonItem = NavigationBarItemsView( + with: [ + .init(image: UIImage(named: "share"), action: (self, #selector(handleSaveAction))), + .init(image: UIImage(named: "copy"), action: (self, #selector(handleCopyAction))), + .init(image: UIImage(systemName: "trash"), action: (self, #selector(handleRemoveAction))) + ] + ) + } +} + +extension ContactKeyDetailViewController { + @objc private final func handleSaveAction() { + let vc = UIActivityViewController( + activityItems: [pubKey.key], + applicationActivities: nil + ) + present(vc, animated: true, completion: nil) + } + + @objc private final func handleCopyAction() { + UIPasteboard.general.string = pubKey.key + showToast("contact_detail_copy".localized) + } + + @objc private final func handleRemoveAction() { + navigationController?.popViewController(animated: true) { [weak self] in + guard let self = self else { return } + self.action?(.delete(self.pubKey)) + } + } +} + +extension ContactKeyDetailViewController: ASTableDelegate, ASTableDataSource { + func tableNode(_: ASTableNode, numberOfRowsInSection section: Int) -> Int { + Part.allCases.count + } + + func tableNode(_: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { + { [weak self] in + guard let self = self, let part = Part(rawValue: indexPath.row) + else { return ASCellNode() } + return self.node(for: part) + } + } +} + +// MARK: - UI +extension ContactKeyDetailViewController { + private func node(for part: Part) -> ASCellNode { + LabelCellNode(title: decorator.attributedTitle(for: part), + text: content(for: part).attributed(.regular(14))) + } + + private func content(for part: Part) -> String { + switch part { + case .key: + return pubKey.key + case .signature: + return string(from: pubKey.lastSig) + case .created: + return string(from: pubKey.created) + case .checked: + return string(from: pubKey.lastChecked) + case .expire: + return string(from: pubKey.expiresOn) + case .longids: + return pubKey.longids.joined(separator: ", ") + case .fingerprints: + return pubKey.fingerprints.joined(separator: ", ") + case .algo: + return pubKey.algo?.algorithm ?? "-" + } + } + + private func string(from date: Date?) -> String { + guard let date = date else { return "-" } + + let df = DateFormatter() + df.dateStyle = .medium + df.timeStyle = .medium + return df.string(from: date) + } +} diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListDecorator.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListDecorator.swift index 2eff9d7c8..8e100bb87 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListDecorator.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListDecorator.swift @@ -11,27 +11,27 @@ import UIKit protocol ContactsListDecoratorType { var title: String { get } - func contactNodeInput(with contact: Contact) -> ContactCellNode.Input + func contactNodeInput(with recipient: RecipientWithPubKeys) -> ContactCellNode.Input } struct ContactsListDecorator: ContactsListDecoratorType { let title = "contacts_screen_title".localized - func contactNodeInput(with contact: Contact) -> ContactCellNode.Input { + func contactNodeInput(with recipient: RecipientWithPubKeys) -> ContactCellNode.Input { let name: String - if let contactName = contact.name, contactName.isNotEmpty { - let components = contactName + if let recipientName = recipient.name, recipientName.isNotEmpty { + let components = recipientName .split(separator: " ") .filter { !$0.contains("@") } if components.isEmpty { - name = nameFrom(email: contact.email) + name = nameFrom(email: recipient.email) } else { name = components.joined(separator: " ") } } else { - name = nameFrom(email: contact.email) + name = nameFrom(email: recipient.email) } let buttonColor = UIColor.colorFor( @@ -39,11 +39,14 @@ struct ContactsListDecorator: ContactsListDecoratorType { lightStyle: .darkGray ) + let keysCount = "%@ public key(s)".localizeWithArguments(recipient.pubKeys.count) + return ContactCellNode.Input( name: name.attributed(.medium(16)), - email: contact.email.attributed(.medium(14)), + email: recipient.email.attributed(.medium(14)), + keys: "(\(keysCount))".attributed(.medium(14), color: .mainTextColor.withAlphaComponent(0.5)), insets: UIEdgeInsets(top: 16, left: 16, bottom: 8, right: 16), - buttonImage: #imageLiteral(resourceName: "trash").tinted(buttonColor) + buttonImage: UIImage(systemName: "trash")?.tinted(buttonColor) ) } diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift index a8b64a71d..33a73b2b1 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift @@ -18,7 +18,8 @@ import FlowCryptUI final class ContactsListViewController: TableNodeViewController { private let decorator: ContactsListDecoratorType private let contactsProvider: LocalContactsProviderType - private var contacts: [Contact] = [] + private var recipients: [RecipientWithPubKeys] = [] + private var selectedIndexPath: IndexPath? init( decorator: ContactsListDecoratorType = ContactsListDecorator(), @@ -39,6 +40,11 @@ final class ContactsListViewController: TableNodeViewController { setupUI() fetchContacts() } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + reloadContacts() + } } extension ContactsListViewController { @@ -48,21 +54,28 @@ extension ContactsListViewController { title = decorator.title } + private func reloadContacts() { + guard let indexPath = selectedIndexPath else { return } + fetchContacts() + node.reloadRows(at: [indexPath], with: .automatic) + selectedIndexPath = nil + } + private func fetchContacts() { - contacts = contactsProvider.getAllContacts() + recipients = contactsProvider.getAllRecipients() } } extension ContactsListViewController: ASTableDelegate, ASTableDataSource { func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { - contacts.count + recipients.count } func tableNode(_: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { return { [weak self] in guard let self = self else { return ASCellNode() } return ContactCellNode( - input: self.decorator.contactNodeInput(with: self.contacts[indexPath.row]), + input: self.decorator.contactNodeInput(with: self.recipients[indexPath.row]), action: { [weak self] in self?.handleDeleteButtonTap(with: indexPath) } @@ -73,7 +86,7 @@ extension ContactsListViewController: ASTableDelegate, ASTableDataSource { } func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { - proceedToKeyDetail(with: indexPath) + proceedToContactDetail(with: indexPath) } } @@ -82,9 +95,9 @@ extension ContactsListViewController { delete(with: .right(indexPath)) } - private func proceedToKeyDetail(with indexPath: IndexPath) { + private func proceedToContactDetail(with indexPath: IndexPath) { let contactDetailViewController = ContactDetailViewController( - contact: contacts[indexPath.row] + recipient: recipients[indexPath.row] ) { [weak self] action in guard case let .delete(contact) = action else { assertionFailure("Action is not implemented") @@ -92,28 +105,28 @@ extension ContactsListViewController { } self?.delete(with: .left(contact)) } - + selectedIndexPath = indexPath navigationController?.pushViewController(contactDetailViewController, animated: true) } - private func delete(with context: Either) { - let contactToRemove: Contact + private func delete(with context: Either) { + let recipientToRemove: RecipientWithPubKeys let indexPathToRemove: IndexPath switch context { - case .left(let contact): - contactToRemove = contact - guard let index = contacts.firstIndex(where: { $0 == contact }) else { + case .left(let recipient): + recipientToRemove = recipient + guard let index = recipients.firstIndex(where: { $0 == recipient }) else { assertionFailure("Can't find index of the contact") return } indexPathToRemove = IndexPath(row: index, section: 0) case .right(let indexPath): indexPathToRemove = indexPath - contactToRemove = contacts[indexPath.row] + recipientToRemove = recipients[indexPath.row] } - contactsProvider.remove(contact: contactToRemove) - contacts.remove(at: indexPathToRemove.row) + contactsProvider.remove(recipient: recipientToRemove) + recipients.remove(at: indexPathToRemove.row) node.deleteRows(at: [indexPathToRemove], with: .left) } } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index fd16e68f4..a45580d2f 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -113,18 +113,18 @@ final class ComposeMessageService { } } - private func getPubKeys(for recepients: [ComposeMessageRecipient]) -> Result<[String], MessageValidationError> { - let pubKeys = recepients.map { - ($0.email, contactsService.retrievePubKey(for: $0.email)) + private func getPubKeys(for recipients: [ComposeMessageRecipient]) -> Result<[String], MessageValidationError> { + let pubKeys = recipients.map { + (email: $0.email, keys: contactsService.retrievePubKeys(for: $0.email)) } - let emailsWithoutPubKeys = pubKeys.filter { $0.1 == nil }.map(\.0) + let emailsWithoutPubKeys = pubKeys.filter { $0.keys.isEmpty }.map(\.email) guard emailsWithoutPubKeys.isEmpty else { return .failure(.noPubRecipients(emailsWithoutPubKeys)) } - return .success(pubKeys.compactMap(\.1)) + return .success(pubKeys.filter({ $0.keys.isNotEmpty }).flatMap(\.keys)) } // MARK: - Encrypt and Send diff --git a/FlowCrypt/Functionality/Services/Contacts Services/ContactsService.swift b/FlowCrypt/Functionality/Services/Contacts Services/ContactsService.swift index 4cfbf678f..0b1495552 100644 --- a/FlowCrypt/Functionality/Services/Contacts Services/ContactsService.swift +++ b/FlowCrypt/Functionality/Services/Contacts Services/ContactsService.swift @@ -18,11 +18,12 @@ protocol ContactsServiceType: PublicKeyProvider, ContactsProviderType { } protocol ContactsProviderType { - func searchContact(with email: String) -> Promise + func searchContact(with email: String) -> Promise } protocol PublicKeyProvider { - func retrievePubKey(for email: String) -> String? + func retrievePubKeys(for email: String) -> [String] + func removePubKey(with fingerprint: String, for email: String) } // MARK: - PROVIDER @@ -41,26 +42,30 @@ struct ContactsService: ContactsServiceType { } extension ContactsService: ContactsProviderType { - func searchContact(with email: String) -> Promise { - guard let contact = localContactsProvider.searchContact(with: email) else { + func searchContact(with email: String) -> Promise { + guard let contact = localContactsProvider.searchRecipient(with: email) else { return searchRemote(for: email) } return Promise(contact) } - private func searchRemote(for email: String) -> Promise { + private func searchRemote(for email: String) -> Promise { pubLookup .lookup(with: email) - .then { contact in - self.localContactsProvider.save(contact: contact) + .then { recipient in + self.localContactsProvider.save(recipient: recipient) } } } extension ContactsService: PublicKeyProvider { - func retrievePubKey(for email: String) -> String? { - let publicKey = localContactsProvider.retrievePubKey(for: email) + func retrievePubKeys(for email: String) -> [String] { + let publicKeys = localContactsProvider.retrievePubKeys(for: email) localContactsProvider.updateLastUsedDate(for: email) - return publicKey + return publicKeys + } + + func removePubKey(with fingerprint: String, for email: String) { + localContactsProvider.removePubKey(with: fingerprint, for: email) } } diff --git a/FlowCrypt/Functionality/Services/Contacts Services/LocalContactsProvider.swift b/FlowCrypt/Functionality/Services/Contacts Services/LocalContactsProvider.swift index 75d0cc111..f36a367b2 100644 --- a/FlowCrypt/Functionality/Services/Contacts Services/LocalContactsProvider.swift +++ b/FlowCrypt/Functionality/Services/Contacts Services/LocalContactsProvider.swift @@ -12,70 +12,82 @@ import RealmSwift protocol LocalContactsProviderType: PublicKeyProvider { func updateLastUsedDate(for email: String) - func searchContact(with email: String) -> Contact? - func save(contact: Contact) - func remove(contact: Contact) - func getAllContacts() -> [Contact] + func searchRecipient(with email: String) -> RecipientWithPubKeys? + func save(recipient: RecipientWithPubKeys) + func remove(recipient: RecipientWithPubKeys) + func getAllRecipients() -> [RecipientWithPubKeys] } struct LocalContactsProvider { - private let localContactsCache: CacheService + private let localContactsCache: CacheService let core: Core init( encryptedStorage: EncryptedStorageType = EncryptedStorage(), core: Core = .shared ) { - self.localContactsCache = CacheService(encryptedStorage: encryptedStorage) + self.localContactsCache = CacheService(encryptedStorage: encryptedStorage) self.core = core } } extension LocalContactsProvider: LocalContactsProviderType { func updateLastUsedDate(for email: String) { - let contact = localContactsCache.realm - .objects(ContactObject.self) - .first(where: { $0.email == email }) + let recipient = find(with: email) try? localContactsCache.realm.write { - contact?.lastUsed = Date() + recipient?.lastUsed = Date() } } - func retrievePubKey(for email: String) -> String? { - localContactsCache.encryptedStorage.storage - .objects(ContactObject.self) - .first(where: { $0.email == email })? - .pubKey + func retrievePubKeys(for email: String) -> [String] { + find(with: email)?.pubKeys + .map { $0.key } ?? [] } - func save(contact: Contact) { - localContactsCache.save(ContactObject(contact)) + func save(recipient: RecipientWithPubKeys) { + localContactsCache.save(RecipientObject(recipient)) } - func remove(contact: Contact) { + func remove(recipient: RecipientWithPubKeys) { localContactsCache.remove( - object: ContactObject(contact), - with: contact.email + object: RecipientObject(recipient), + with: recipient.email ) } - func searchContact(with email: String) -> Contact? { + func searchRecipient(with email: String) -> RecipientWithPubKeys? { + guard let recipientObject = find(with: email) else { return nil } + return RecipientWithPubKeys(recipientObject) + } + + func getAllRecipients() -> [RecipientWithPubKeys] { localContactsCache.realm - .objects(ContactObject.self) - .first(where: { $0.email == email }) - .map { Contact($0) } + .objects(RecipientObject.self) + .map { object in + let keyDetails = object.pubKeys + .compactMap { try? core.parseKeys(armoredOrBinary: $0.key.data()).keyDetails } + .flatMap { $0 } + return RecipientWithPubKeys(object, keyDetails: Array(keyDetails)) + } + .sorted(by: { $0.email > $1.email }) } - func getAllContacts() -> [Contact] { - Array( - localContactsCache.realm - .objects(ContactObject.self) - .map { - let keyDetail = try? core.parseKeys(armoredOrBinary: $0.pubKey.data()).keyDetails.first - return Contact($0, keyDetail: keyDetail) + func removePubKey(with fingerprint: String, for email: String) { + find(with: email)? + .pubKeys + .filter { $0.fingerprint == fingerprint } + .forEach { key in + try? localContactsCache.realm.write { + localContactsCache.realm.delete(key) } - .sorted(by: { $0.email > $1.email }) - ) + } + } +} + +extension LocalContactsProvider { + private func find(with email: String) -> RecipientObject? { + localContactsCache.realm.object(ofType: RecipientObject.self, + forPrimaryKey: email) } } diff --git a/FlowCrypt/Functionality/Services/Contacts Services/Models/Contact.swift b/FlowCrypt/Functionality/Services/Contacts Services/Models/Contact.swift deleted file mode 100644 index 64510ba91..000000000 --- a/FlowCrypt/Functionality/Services/Contacts Services/Models/Contact.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// Contact.swift -// FlowCrypt -// -// Created by Anton Kharchevskyi on 21/08/2020. -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. -// - -import Foundation - -struct Contact { - let email: String - /// name if known - let name: String? - /// public key - let pubKey: String - /// will be provided later - let pubKeyLastSig: Date? - /// the date when pubkey was retrieved from Attester, or nil - let pubkeyLastChecked: Date? - /// pubkey expiration date - let pubkeyExpiresOn: Date? - /// all pubkey longids, comma-separated - let longids: [String] - var longid: String? { longids.first } - - /// last time an email was sent to this contact, update when email is sent - let lastUsed: Date? - - /// all pubkey fingerprints, comma-separated - let fingerprints: [String] - /// first pubkey fingerprint - var fingerprint: String? { fingerprints.first } - - /// pubkey created date - let pubkeyCreated: Date? - - let algo: KeyAlgo? -} - -extension Contact { - init(_ contactObject: ContactObject, keyDetail: KeyDetails? = nil) { - self.email = contactObject.email - self.name = contactObject.name.nilIfEmpty - self.pubKey = contactObject.pubKey - self.pubKeyLastSig = contactObject.pubKeyLastSig - self.pubkeyLastChecked = contactObject.pubkeyLastChecked - self.pubkeyExpiresOn = contactObject.pubkeyExpiresOn - self.lastUsed = contactObject.lastUsed - self.longids = contactObject.longids.map(\.value) - self.fingerprints = contactObject.fingerprints.split(separator: ",").map(String.init) - self.pubkeyCreated = contactObject.pubkeyCreated - self.algo = keyDetail?.algo - } -} - -extension Contact { - init(email: String, keyDetail: KeyDetails) { - let keyIds = keyDetail.ids - let longids = keyIds.map(\.longid) - let fingerprints = keyIds.map(\.fingerprint) - - self.email = email - self.name = keyDetail.users.first ?? email - self.pubKey = keyDetail.public - self.pubKeyLastSig = keyDetail.lastModified.map { Date(timeIntervalSince1970: TimeInterval($0)) } - self.pubkeyLastChecked = Date() - self.pubkeyExpiresOn = keyDetail.expiration.map { Date(timeIntervalSince1970: TimeInterval($0)) } - self.longids = longids - self.lastUsed = nil - self.fingerprints = fingerprints - self.pubkeyCreated = Date(timeIntervalSince1970: Double(keyDetail.created)) - self.algo = keyDetail.algo - } -} - -extension Contact: Equatable { - static func == (lhs: Contact, rhs: Contact) -> Bool { - lhs.email == rhs.email - } -} diff --git a/FlowCrypt/Functionality/Services/Contacts Services/Models/PubKey.swift b/FlowCrypt/Functionality/Services/Contacts Services/Models/PubKey.swift new file mode 100644 index 000000000..7c72e10f3 --- /dev/null +++ b/FlowCrypt/Functionality/Services/Contacts Services/Models/PubKey.swift @@ -0,0 +1,57 @@ +// +// ContactKey.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 11/10/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Foundation + +struct PubKey { + let key: String + /// will be provided later + let lastSig: Date? + /// the date when key was retrieved from a public key server, or nil + let lastChecked: Date? + /// expiration date + let expiresOn: Date? + /// all key longids + let longids: [String] + /// all key fingerprints + let fingerprints: [String] + /// key created date + let created: Date? + /// key algo + let algo: KeyAlgo? +} + +extension PubKey { + /// first key longid + var longid: String? { longids.first } + /// first key fingerprint + var fingerprint: String? { fingerprints.first } +} + +extension PubKey { + init(keyDetails: KeyDetails) { + let keyIds = keyDetails.ids + let longids = keyIds.map(\.longid) + let fingerprints = keyIds.map(\.fingerprint) + + self.init(key: keyDetails.public, + lastSig: keyDetails.lastModified.map { Date(timeIntervalSince1970: TimeInterval($0)) }, + lastChecked: Date(), + expiresOn: keyDetails.expiration.map { Date(timeIntervalSince1970: TimeInterval($0)) }, + longids: longids, + fingerprints: fingerprints, + created: Date(timeIntervalSince1970: Double(keyDetails.created)), + algo: keyDetails.algo) + } +} + +extension PubKey: Equatable { + static func == (lhs: PubKey, rhs: PubKey) -> Bool { + lhs.key == rhs.key + } +} diff --git a/FlowCrypt/Functionality/Services/Contacts Services/Models/RecipientWithPubKeys.swift b/FlowCrypt/Functionality/Services/Contacts Services/Models/RecipientWithPubKeys.swift new file mode 100644 index 000000000..f6dd56e5f --- /dev/null +++ b/FlowCrypt/Functionality/Services/Contacts Services/Models/RecipientWithPubKeys.swift @@ -0,0 +1,49 @@ +// +// Contact.swift +// FlowCrypt +// +// Created by Anton Kharchevskyi on 21/08/2020. +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Foundation + +struct RecipientWithPubKeys { + let email: String + /// name if known + let name: String? + /// last time an email was sent to this contact, update when email is sent + let lastUsed: Date? + /// public keys + var pubKeys: [PubKey] +} + +extension RecipientWithPubKeys { + init(_ recipientObject: RecipientObject, keyDetails: [KeyDetails] = []) { + self.email = recipientObject.email + self.name = recipientObject.name.nilIfEmpty + self.lastUsed = recipientObject.lastUsed + self.pubKeys = keyDetails.map(PubKey.init) + } +} + +extension RecipientWithPubKeys { + init(email: String, keyDetails: [KeyDetails]) { + self.email = email + self.name = keyDetails.first?.users.first ?? email + self.lastUsed = nil + self.pubKeys = keyDetails.map(PubKey.init) + } +} + +extension RecipientWithPubKeys { + mutating func remove(pubKey: PubKey) { + pubKeys.removeAll(where: { $0 == pubKey }) + } +} + +extension RecipientWithPubKeys: Equatable { + static func == (lhs: RecipientWithPubKeys, rhs: RecipientWithPubKeys) -> Bool { + lhs.email == rhs.email + } +} diff --git a/FlowCrypt/Functionality/WKDURLs/PubLookup.swift b/FlowCrypt/Functionality/WKDURLs/PubLookup.swift index ab7b4201b..1d18560ca 100644 --- a/FlowCrypt/Functionality/WKDURLs/PubLookup.swift +++ b/FlowCrypt/Functionality/WKDURLs/PubLookup.swift @@ -9,7 +9,7 @@ import Promises protocol PubLookupType { - func lookup(with email: String) -> Promise + func lookup(with email: String) -> Promise } class PubLookup: PubLookupType { @@ -24,35 +24,20 @@ class PubLookup: PubLookupType { self.attesterApi = attesterApi } - func lookup(with email: String) -> Promise { - Promise { resolve, _ in - let keyDetails = try awaitPromise(self.getKeyDetails(email)) - // TODO: - we are blindly choosing .first public key, in the future we should return [Contact] - // then eg encrypt for all returned Contacts - // also stop throwing below - no point. Return - // empty array then handle downstream - guard let keyDetail = keyDetails.first else { - throw ContactsError.keyMissing - } - resolve(Contact(email: email, keyDetail: keyDetail)) - } - } - - private func getKeyDetails(_ email: String) -> Promise<[KeyDetails]> { - - Promise<[KeyDetails]> { [weak self] resolve, _ in + func lookup(with email: String) -> Promise { + Promise { [weak self] resolve, _ in guard let self = self else { - resolve([]) + resolve(RecipientWithPubKeys(email: email, keyDetails: [])) return } let wkdResult = try awaitPromise(self.wkd.lookupEmail(email)) if !wkdResult.isEmpty { - resolve(wkdResult) + resolve(RecipientWithPubKeys(email: email, keyDetails: wkdResult)) } let attesterResult = try awaitPromise(self.attesterApi.lookupEmail(email: email)) - resolve(attesterResult) + resolve(RecipientWithPubKeys(email: email, keyDetails: attesterResult)) } } } diff --git a/FlowCrypt/Models/Realm Models/ContactKeyObject.swift b/FlowCrypt/Models/Realm Models/ContactKeyObject.swift new file mode 100644 index 000000000..3d2e84691 --- /dev/null +++ b/FlowCrypt/Models/Realm Models/ContactKeyObject.swift @@ -0,0 +1,58 @@ +// +// ContactKeyObject.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 11/10/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Foundation +import RealmSwift + +final class PubKeyObject: Object { + @Persisted(primaryKey: true) var key: String = "" + + @Persisted var lastSig: Date? + @Persisted var lastChecked: Date? + @Persisted var expiresOn: Date? + @Persisted var longids: List + @Persisted var fingerprints: List + @Persisted var created: Date? + + convenience init(key: String, + lastSig: Date? = nil, + lastChecked: Date? = nil, + expiresOn: Date? = nil, + longids: [String] = [], + fingerprints: [String] = [], + created: Date? = nil) { + self.init() + + self.key = key + self.lastSig = lastSig + self.lastChecked = lastChecked + self.expiresOn = expiresOn + self.created = created + + longids.forEach { self.longids.append($0) } + fingerprints.forEach { self.fingerprints.append($0) } + } +} + +extension PubKeyObject { + convenience init(_ key: PubKey) { + self.init( + key: key.key, + lastSig: key.lastSig, + lastChecked: key.lastChecked, + expiresOn: key.expiresOn, + longids: key.longids, + fingerprints: key.fingerprints, + created: key.created + ) + } +} + +extension PubKeyObject { + var fingerprint: String? { fingerprints.first } +} diff --git a/FlowCrypt/Models/Realm Models/ContactObject.swift b/FlowCrypt/Models/Realm Models/ContactObject.swift index 3aec666e8..4c4d313c5 100644 --- a/FlowCrypt/Models/Realm Models/ContactObject.swift +++ b/FlowCrypt/Models/Realm Models/ContactObject.swift @@ -1,5 +1,5 @@ // -// ContactObject.swift +// RecipientObject.swift // FlowCrypt // // Created by Anton Kharchevskyi on 21/08/2020. @@ -10,7 +10,7 @@ import Foundation import RealmSwift final class LongId: Object { - @objc dynamic var value: String = "" + @Persisted var value: String = "" convenience init(value: String) { self.init() @@ -18,76 +18,44 @@ final class LongId: Object { } } -final class ContactObject: Object { - @objc dynamic var email: String = "" - @objc dynamic var pubKey: String = "" +final class RecipientObject: Object { + @Persisted(primaryKey: true) var email: String = "" - @objc dynamic var name: String? - - @objc dynamic var pubkeyExpiresOn: Date? - @objc dynamic var pubKeyLastSig: Date? - @objc dynamic var pubkeyLastChecked: Date? - @objc dynamic var pubkeyCreated: Date? - @objc dynamic var lastUsed: Date? - - /// all pubkey fingerprints, comma-separated - @objc dynamic var fingerprints: String = "" - - let longids = List() + @Persisted var name: String? + @Persisted var lastUsed: Date? + @Persisted var pubKeys = List() convenience init( email: String, name: String?, - pubKey: String, - pubKeyLastSig: Date?, - pubkeyLastChecked: Date?, - pubkeyExpiresOn: Date?, lastUsed: Date?, - pubkeyCreated: Date?, - longids: [String], - fingerprints: [String] + keys: [PubKey] ) { self.init() self.email = email self.name = name ?? "" - self.pubKey = pubKey - self.pubkeyExpiresOn = pubkeyExpiresOn - self.pubKeyLastSig = pubKeyLastSig - self.pubkeyLastChecked = pubkeyLastChecked - self.pubkeyCreated = pubkeyCreated self.lastUsed = lastUsed - self.fingerprints = fingerprints.joined(separator: ",") - longids - .map(LongId.init) + keys + .map(PubKeyObject.init) .forEach { - self.longids.append($0) + self.pubKeys.append($0) } } - - override class func primaryKey() -> String? { - "email" - } } -extension ContactObject { - convenience init(_ contact: Contact) { +extension RecipientObject { + convenience init(_ recipient: RecipientWithPubKeys) { self.init( - email: contact.email, - name: contact.name, - pubKey: contact.pubKey, - pubKeyLastSig: contact.pubKeyLastSig, - pubkeyLastChecked: contact.pubkeyLastChecked, - pubkeyExpiresOn: contact.pubkeyExpiresOn, - lastUsed: contact.lastUsed, - pubkeyCreated: contact.pubkeyCreated, - longids: contact.longids, - fingerprints: contact.fingerprints + email: recipient.email, + name: recipient.name, + lastUsed: recipient.lastUsed, + keys: recipient.pubKeys ) } } -extension ContactObject: CachedObject { +extension RecipientObject: CachedObject { // Contacts can be shared between accounts // https://github.com/FlowCrypt/flowcrypt-ios/issues/269 var activeUser: UserObject? { nil } diff --git a/FlowCrypt/Resources/Localizable.stringsdict b/FlowCrypt/Resources/Localizable.stringsdict index e033f1e37..a639e660d 100644 --- a/FlowCrypt/Resources/Localizable.stringsdict +++ b/FlowCrypt/Resources/Localizable.stringsdict @@ -20,5 +20,23 @@ Fetched %d keys on EKM + %@ public key(s) + + NSStringLocalizedFormatKey + %#@keys@ + keys + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + zero + No public keys + one + %d public key + other + %d public keys + + diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 8fdd4d298..f523a3773 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -141,8 +141,19 @@ // Contacts "contacts_screen_title" = "Contacts"; -"contact_detail_screen_title" = "Public Key"; +"contact_detail_screen_title" = "Public Keys"; "contact_detail_copy" = "Public Key copied to clipboard"; +"contact_key_detail_screen_title" = "Public Key"; + +// Contacts keys +"contact_key_pub" = "Key"; +"contact_key_signature" = "Signature"; +"contact_key_fetched" = "Last fetched"; +"contact_key_expires" = "Expires"; +"contact_key_longids" = "Longids"; +"contact_key_fingerprints" = "Fingerprints"; +"contact_key_created" = "Created"; +"contact_key_algo" = "Algo"; // Key Settings "key_settings_title" = "Keys"; diff --git a/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift b/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift index d2715b3c6..e24d8b455 100644 --- a/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift +++ b/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift @@ -201,8 +201,8 @@ class ComposeMessageServiceTests: XCTestCase { } recipients.forEach { recipient in - contactsService.retrievePubKeyResult = { _ in - nil + contactsService.retrievePubKeysResult = { _ in + [] } } @@ -230,11 +230,11 @@ class ComposeMessageServiceTests: XCTestCase { let recWithoutPubKey = recipients[0].email recipients.forEach { _ in - contactsService.retrievePubKeyResult = { recipient in + contactsService.retrievePubKeysResult = { recipient in if recipient == recWithoutPubKey { - return nil + return [] } - return "recipient pub key" + return ["recipient pub key"] } } @@ -261,8 +261,8 @@ class ComposeMessageServiceTests: XCTestCase { } recipients.enumerated().forEach { (element, index) in - contactsService.retrievePubKeyResult = { recipient in - "pubKey" + contactsService.retrievePubKeysResult = { recipient in + ["pubKey"] } } diff --git a/FlowCryptAppTests/Mocks/ContactsServiceMock.swift b/FlowCryptAppTests/Mocks/ContactsServiceMock.swift index 07b9a1fb0..5d0ee9147 100644 --- a/FlowCryptAppTests/Mocks/ContactsServiceMock.swift +++ b/FlowCryptAppTests/Mocks/ContactsServiceMock.swift @@ -11,13 +11,15 @@ import Promises @testable import FlowCrypt class ContactsServiceMock: ContactsServiceType { - var retrievePubKeyResult: ((String) -> (String?))! - func retrievePubKey(for email: String) -> String? { - retrievePubKeyResult(email) + var retrievePubKeysResult: ((String) -> ([String]))! + func retrievePubKeys(for email: String) -> [String] { + retrievePubKeysResult(email) } - var searchContactResult: Result! - func searchContact(with email: String) -> Promise { - Promise.resolveAfter(with: searchContactResult) + var searchContactResult: Result! + func searchContact(with email: String) -> Promise { + Promise.resolveAfter(with: searchContactResult) } + + func removePubKey(with fingerprint: String, for email: String) {} } diff --git a/FlowCryptUI/Cell Nodes/ContactCellNode.swift b/FlowCryptUI/Cell Nodes/ContactCellNode.swift index 45d1bf555..37858f502 100644 --- a/FlowCryptUI/Cell Nodes/ContactCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ContactCellNode.swift @@ -12,17 +12,20 @@ public final class ContactCellNode: CellNode { public struct Input { let name: NSAttributedString? let email: NSAttributedString + let keys: NSAttributedString let insets: UIEdgeInsets let buttonImage: UIImage? public init( name: NSAttributedString?, email: NSAttributedString, + keys: NSAttributedString, insets: UIEdgeInsets, buttonImage: UIImage? ) { self.name = name self.email = email + self.keys = keys self.insets = insets self.buttonImage = buttonImage } @@ -30,6 +33,7 @@ public final class ContactCellNode: CellNode { private let nameNode = ASTextNode2() private let emailNode = ASTextNode2() + private let keysNode = ASTextNode2() private let buttonNode = ASButtonNode() private let input: Input @@ -42,6 +46,7 @@ public final class ContactCellNode: CellNode { nameNode.attributedText = input.name emailNode.attributedText = input.email + keysNode.attributedText = input.keys buttonNode.setImage(input.buttonImage, for: .normal) buttonNode.addTarget(self, action: #selector(handleButtonTap), forControlEvents: .touchUpInside) } @@ -56,12 +61,19 @@ public final class ContactCellNode: CellNode { emailNode.style.flexGrow = 1 children = [emailNode, buttonNode] } else { + let infoStack = ASStackLayoutSpec( + direction: .horizontal, + spacing: 4, + justifyContent: .start, + alignItems: .start, + children: [emailNode, keysNode] + ) let nameStack = ASStackLayoutSpec( direction: .vertical, spacing: 8, justifyContent: .start, alignItems: .start, - children: [nameNode, emailNode] + children: [nameNode, infoStack] ) nameStack.style.flexGrow = 1 children = [nameStack, buttonNode] diff --git a/FlowCryptUI/Cell Nodes/ContactDetailNode.swift b/FlowCryptUI/Cell Nodes/ContactDetailNode.swift deleted file mode 100644 index 670c1716b..000000000 --- a/FlowCryptUI/Cell Nodes/ContactDetailNode.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// ContactDetailNode.swift -// FlowCryptUI -// -// Created by Anton Kharchevskyi on 23/08/2020. -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. -// - -import AsyncDisplayKit - -public final class ContactDetailNode: CellNode { - public struct Input { - let user: NSAttributedString - let ids: NSAttributedString - let fingerprints: NSAttributedString - let algoInfo: NSAttributedString? - let created: NSAttributedString? - - public init( - user: NSAttributedString, - ids: NSAttributedString, - fingerprints: NSAttributedString, - algoInfo: NSAttributedString?, - created: NSAttributedString? - ) { - self.user = user - self.ids = ids - self.fingerprints = fingerprints - self.algoInfo = algoInfo - self.created = created - } - } - - private let input: Input - - private let userTitleNode = ASTextNode2() - private let userNode = ASTextNode2() - - private let idsTitleNode = ASTextNode2() - private let idsNode = ASTextNode2() - - private let fingerprintsTitleNode = ASTextNode2() - private let fingerprintsNode = ASTextNode2() - - private let algoTitleNode = ASTextNode2() - private let algoNode = ASTextNode2() - - private let createdTitleNode = ASTextNode2() - private let createdNode = ASTextNode2() - - public init(input: Input) { - self.input = input - userTitleNode.attributedText = "User:".attributed(.bold(16)) - userNode.attributedText = input.user - - idsTitleNode.attributedText = "Longids:".attributed(.bold(16)) - idsNode.attributedText = input.ids - - fingerprintsTitleNode.attributedText = "Fingerprints:".attributed(.bold(16)) - fingerprintsNode.attributedText = input.fingerprints - - algoTitleNode.attributedText = "Algorithm:".attributed(.bold(16)) - algoNode.attributedText = input.algoInfo - - createdTitleNode.attributedText = "Created:".attributed(.bold(16)) - createdNode.attributedText = input.created - } - - public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { - let specs = [ - [userTitleNode, userNode], - [idsTitleNode, idsNode], - [fingerprintsTitleNode, fingerprintsNode], - [algoTitleNode, algoNode], - [createdTitleNode, createdNode] - ] - .map { - ASStackLayoutSpec( - direction: .vertical, - spacing: 4, - justifyContent: .start, - alignItems: .start, - children: $0 - ) - } - - return ASInsetLayoutSpec( - insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), - child: ASStackLayoutSpec( - direction: .vertical, - spacing: 12, - justifyContent: .start, - alignItems: .start, - children: specs - ) - ) - } -} diff --git a/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift b/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift new file mode 100644 index 000000000..8e546a7b5 --- /dev/null +++ b/FlowCryptUI/Cell Nodes/ContactKeyCellNode.swift @@ -0,0 +1,106 @@ +// +// ContactKeyCellNode.swift +// FlowCryptUI +// +// Created by Roma Sosnovsky on 11/10/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + + +import AsyncDisplayKit +import Foundation + +public final class ContactKeyCellNode: CellNode { + public struct Input { + let fingerprint: NSAttributedString + let createdAt: NSAttributedString + let expires: NSAttributedString + + public init( + fingerprint: NSAttributedString, + createdAt: NSAttributedString, + expires: NSAttributedString + ) { + self.fingerprint = fingerprint + self.createdAt = createdAt + self.expires = expires + } + } + + private let fingerprintTitleNode = ASTextNode2() + private let fingerprintNode = ASTextNode2() + + private let createdAtTitleNode = ASTextNode2() + private let createdAtNode = ASTextNode2() + + private let expiresTitleNode = ASTextNode2() + private let expiresNode = ASTextNode2() + + private let borderNode = ASDisplayNode() + + private let input: Input + + public init(input: Input) { + self.input = input + + super.init() + + fingerprintTitleNode.attributedText = "Fingerprint:".attributed(.bold(16)) + fingerprintNode.attributedText = input.fingerprint + + createdAtTitleNode.attributedText = "Created:".attributed(.bold(16)) + createdAtNode.attributedText = input.createdAt + + expiresTitleNode.attributedText = "Expires:".attributed(.bold(16)) + expiresNode.attributedText = input.expires + + borderNode.borderWidth = 1.0 + borderNode.cornerRadius = 8.0 + borderNode.borderColor = UIColor.lightGray.cgColor + borderNode.isUserInteractionEnabled = false + } + + public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + let specs = [ + [fingerprintTitleNode, fingerprintNode], + [createdAtTitleNode, createdAtNode], + [expiresTitleNode, expiresNode] + ].map { + ASStackLayoutSpec( + direction: .vertical, + spacing: 4, + justifyContent: .start, + alignItems: .start, + children: $0 + ) + } + + let stack = ASStackLayoutSpec( + direction: .vertical, + spacing: 8, + justifyContent: .start, + alignItems: .start, + children: specs + ) + + let borderInset = UIEdgeInsets.side(8) + + let resultSpec = ASInsetLayoutSpec( + insets: UIEdgeInsets( + top: 10 + borderInset.top, + left: 12 + borderInset.left, + bottom: 10 + borderInset.bottom, + right: 12 + borderInset.right + ), + child: stack + ) + + return ASOverlayLayoutSpec( + child: resultSpec, + overlay: ASInsetLayoutSpec( + insets: borderInset, + child: borderNode + ) + ) + } +} diff --git a/FlowCryptUI/Cell Nodes/ContactUserCellNode.swift b/FlowCryptUI/Cell Nodes/ContactUserCellNode.swift new file mode 100644 index 000000000..21dd2aa0a --- /dev/null +++ b/FlowCryptUI/Cell Nodes/ContactUserCellNode.swift @@ -0,0 +1,43 @@ +// +// ContactUserCellNode.swift +// FlowCryptUI +// +// Created by Anton Kharchevskyi on 23/08/2020. +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit + +public final class ContactUserCellNode: CellNode { + public struct Input { + let user: NSAttributedString + + public init(user: NSAttributedString) { + self.user = user + } + } + + private let input: Input + + private let userTitleNode = ASTextNode2() + private let userNode = ASTextNode2() + + public init(input: Input) { + self.input = input + userTitleNode.attributedText = "User:".attributed(.bold(16)) + userNode.attributedText = input.user + } + + public override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { + ASInsetLayoutSpec( + insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), + child: ASStackLayoutSpec( + direction: .vertical, + spacing: 4, + justifyContent: .start, + alignItems: .start, + children: [userTitleNode, userNode] + ) + ) + } +} diff --git a/FlowCryptUI/Cell Nodes/LabelCellNode.swift b/FlowCryptUI/Cell Nodes/LabelCellNode.swift new file mode 100644 index 000000000..5df464451 --- /dev/null +++ b/FlowCryptUI/Cell Nodes/LabelCellNode.swift @@ -0,0 +1,37 @@ +// +// LabelCellNode.swift +// FlowCryptUI +// +// Created by Roma Sosnovsky on 13/10/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit + +public final class LabelCellNode: CellNode { + private let titleNode = ASTextNode2() + private let textNode = ASTextNode2() + private let insets: UIEdgeInsets + + public init(title: NSAttributedString, + text: NSAttributedString, + insets: UIEdgeInsets = UIEdgeInsets.side(8)) { + self.insets = insets + super.init() + titleNode.attributedText = title + textNode.attributedText = text + } + + public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + ASInsetLayoutSpec( + insets: insets, + child: ASStackLayoutSpec( + direction: .vertical, + spacing: 4, + justifyContent: .start, + alignItems: .start, + children: [titleNode, textNode] + ) + ) + } +} diff --git a/Gemfile.lock b/Gemfile.lock index 8ad095581..5f04e7bd6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,13 +17,13 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.507.0) + aws-partitions (1.516.0) aws-sdk-core (3.121.1) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.48.0) + aws-sdk-kms (1.49.0) aws-sdk-core (~> 3, >= 3.120.0) aws-sigv4 (~> 1.1) aws-sdk-s3 (1.103.0) @@ -82,11 +82,11 @@ GEM domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) - emoji_regex (3.2.2) + emoji_regex (3.2.3) escape (0.0.4) - ethon (0.14.0) + ethon (0.15.0) ffi (>= 1.15.0) - excon (0.85.0) + excon (0.87.0) faraday (1.8.0) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -109,10 +109,10 @@ GEM faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - faraday_middleware (1.1.0) + faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.5) - fastlane (2.195.0) + fastlane (2.196.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -156,7 +156,7 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.11.0) + google-apis-androidpublisher_v3 (0.12.0) google-apis-core (>= 0.4, < 2.a) google-apis-core (0.4.1) addressable (~> 2.5, >= 2.5.1) @@ -202,14 +202,14 @@ GEM i18n (1.8.10) concurrent-ruby (~> 1.0) jmespath (1.4.0) - json (2.5.1) - jwt (2.2.3) + json (2.6.0) + jwt (2.3.0) memoist (0.16.2) mime-types (3.3.1) mime-types-data (~> 3.2015) mime-types-data (3.2021.0901) mini_magick (4.11.0) - mini_mime (1.1.1) + mini_mime (1.1.2) minitest (5.14.4) molinillo (0.8.0) multi_json (1.15.0) diff --git a/Podfile b/Podfile index 6e59ed2e8..b813e97e4 100644 --- a/Podfile +++ b/Podfile @@ -35,8 +35,6 @@ post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' - # config.build_settings['ONLY_ACTIVE_ARCH'] = 'NO' - # config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES' end end end