Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions FlowCrypt.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
32DCAF95A6A329C3136B1C8E /* Imap+msg.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA55C094E9745AA1FD210 /* Imap+msg.swift */; };
32DCAF9DA9EC47798DF8BB73 /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA9701B2D5052225A0414 /* SignInViewController.swift */; };
50531BE42629B9A80039BAE9 /* AttachmentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50531BE32629B9A80039BAE9 /* AttachmentNode.swift */; };
5109A77C272153B400D2CEB9 /* LeftAlignedCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5109A77B272153B400D2CEB9 /* LeftAlignedCollectionViewFlowLayout.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 */; };
Expand Down Expand Up @@ -466,6 +467,7 @@
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 = "<group>"; };
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 = "<group>"; };
50531BE32629B9A80039BAE9 /* AttachmentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentNode.swift; sourceTree = "<group>"; };
5109A77B272153B400D2CEB9 /* LeftAlignedCollectionViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftAlignedCollectionViewFlowLayout.swift; sourceTree = "<group>"; };
5133B66F2716320F00C95463 /* ContactKeyDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactKeyDetailViewController.swift; sourceTree = "<group>"; };
5133B6712716321F00C95463 /* ContactKeyDetailDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactKeyDetailDecorator.swift; sourceTree = "<group>"; };
5133B6732716E5EA00C95463 /* LabelCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelCellNode.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1833,6 +1835,7 @@
9FD22A1E230FEFC6005067A6 /* NavigationBarActionButton.swift */,
D28655942423BFF60066F52E /* SideMenuOptionalView.swift */,
D24FAFA32520BF9100BF46C5 /* CheckBoxCircleView.swift */,
5109A77B272153B400D2CEB9 /* LeftAlignedCollectionViewFlowLayout.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -2735,6 +2738,7 @@
D26F132724509EB6009175BA /* RecipientEmailsCellNodeInput.swift in Sources */,
D23C46F623FB44D8008211FB /* DividerCellNode.swift in Sources */,
D27177512425678F00BDA9A9 /* KeySettingCellNode.swift in Sources */,
5109A77C272153B400D2CEB9 /* LeftAlignedCollectionViewFlowLayout.swift in Sources */,
D27177462424D59800BDA9A9 /* InboxCellNode.swift in Sources */,
D27177472424D59800BDA9A9 /* TextCellNode.swift in Sources */,
D211CE6E23FC354200D1CE38 /* CellNode.swift in Sources */,
Expand Down
23 changes: 13 additions & 10 deletions FlowCrypt/Controllers/Compose/ComposeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,6 @@ extension ComposeViewController {
private func handleEditingChanged(with text: String?) {
guard let text = text, text.isNotEmpty else {
search.send("")
updateState(with: .main)
return
}

Expand All @@ -598,13 +597,15 @@ extension ComposeViewController {
// MARK: - Action Handling
extension ComposeViewController {
private func searchEmail(with query: String) {
cloudContactProvider.searchContacts(query: query)
.then(on: .main) { [weak self] emails in
let state: State = emails.isNotEmpty
? .searchEmails(emails)
: .main
self?.updateState(with: state)
}
Task {
let localEmails = contactsService.searchContacts(query: query)
let cloudEmails = try? await cloudContactProvider.searchContacts(query: query)
let emails = Set([localEmails, cloudEmails].compactMap { $0 }.flatMap { $0 })
let state: State = emails.isNotEmpty
? .searchEmails(Array(emails))
: .main
updateState(with: state)
}
}

private func evaluate(recipient: ComposeMessageRecipient) {
Expand Down Expand Up @@ -712,11 +713,13 @@ extension ComposeViewController {
private func updateState(with newState: State) {
state = newState

node.reloadSections([1], with: .automatic)

switch state {
case .main:
node.reloadSections([0, 1], with: .fade)
node.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
case .searchEmails:
node.reloadSections([1], with: .fade)
break
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@
//

import FlowCryptCommon
import Promises
import GoogleAPIClientForREST_PeopleService

protocol CloudContactsProvider {
func searchContacts(query: String) -> Promise<[String]>
func searchContacts(query: String) async throws -> [String]
}

enum CloudContactsProviderError: Error {
Expand All @@ -37,38 +36,47 @@ final class UserContactsProvider {

init(userService: GoogleUserServiceType = GoogleUserService()) {
self.userService = userService

// Warmup query for contacts cache
_ = self.searchContacts(query: "")

runWarmupQuery()
}

private func runWarmupQuery() {
Task {
// Warmup query for google contacts cache
_ = try? await searchContacts(query: "")
}
}
}

extension UserContactsProvider: CloudContactsProvider {
func searchContacts(query: String) -> Promise<[String]> {
func searchContacts(query: String) async throws -> [String] {
let searchQuery = GTLRPeopleServiceQuery_PeopleSearchContacts.query()
searchQuery.readMask = "names,emailAddresses"
searchQuery.query = query

return Promise<[String]> { resolve, reject in
return try await withCheckedThrowingContinuation { continuation in
self.peopleService.executeQuery(searchQuery) { _, data, error in
if let error = error {
return reject(CloudContactsProviderError.providerError(error))
continuation.resume(throwing: CloudContactsProviderError.providerError(error))
return
}

guard let response = data as? GTLRPeopleService_SearchResponse else {
return reject(AppErr.cast("GTLRPeopleService_SearchResponse"))
continuation.resume(throwing: AppErr.cast("GTLRPeopleService_SearchResponse"))
return
}

guard let contacts = response.results else {
return reject(CloudContactsProviderError.failedToParseData(data))
continuation.resume(throwing: CloudContactsProviderError.failedToParseData(data))
return
}

let emails = contacts
.compactMap { $0.person?.emailAddresses }
.flatMap { $0 }
.compactMap { $0.value }

resolve(emails)
continuation.resume(returning: emails)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ protocol ContactsServiceType: PublicKeyProvider, ContactsProviderType {

protocol ContactsProviderType {
func searchContact(with email: String) -> Promise<RecipientWithPubKeys>
func searchContacts(query: String) -> [String]
}

protocol PublicKeyProvider {
Expand Down Expand Up @@ -54,6 +55,9 @@ extension ContactsService: ContactsProviderType {
return Promise(contact)
}

func searchContacts(query: String) -> [String] {
localContactsProvider.searchEmails(query: query)
}
}

extension ContactsService: PublicKeyProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import RealmSwift
protocol LocalContactsProviderType: PublicKeyProvider {
func updateLastUsedDate(for email: String)
func searchRecipient(with email: String) -> RecipientWithPubKeys?
func searchEmails(query: String) -> [String]
func save(recipient: RecipientWithPubKeys)
func remove(recipient: RecipientWithPubKeys)
func updateKeys(for recipient: RecipientWithPubKeys)
Expand Down Expand Up @@ -78,6 +79,13 @@ extension LocalContactsProvider: LocalContactsProviderType {
return RecipientWithPubKeys(recipientObject)
}

func searchEmails(query: String) -> [String] {
localContactsCache.realm
.objects(RecipientObject.self)
.filter("email contains[c] %@", query)
.map(\.email)
}

func getAllRecipients() -> [RecipientWithPubKeys] {
localContactsCache.realm
.objects(RecipientObject.self)
Expand Down
1 change: 1 addition & 0 deletions FlowCryptAppTests/Mocks/ContactsServiceMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class ContactsServiceMock: ContactsServiceType {
func searchContact(with email: String) -> Promise<RecipientWithPubKeys> {
Promise<RecipientWithPubKeys>.resolveAfter(with: searchContactResult)
}
func searchContacts(query: String) -> [String] { [] }

func removePubKey(with fingerprint: String, for email: String) {}
}
12 changes: 2 additions & 10 deletions FlowCryptUI/Cell Nodes/RecipientEmailNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ final class RecipientEmailNode: CellNode {

let titleNode = ASTextNode()
let input: Input
let displayNode = ASDisplayNode()
let imageNode = ASImageNode()

private var onTap: ((Tap) -> Void)?
Expand All @@ -44,7 +43,6 @@ final class RecipientEmailNode: CellNode {
titleNode.borderColor = input.recipient.state.borderColor.cgColor
titleNode.textContainerInset = RecipientEmailNode.Constants.titleInsets

displayNode.backgroundColor = .clear
imageNode.image = input.recipient.state.stateImage
imageNode.alpha = 0
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
Expand Down Expand Up @@ -98,18 +96,12 @@ final class RecipientEmailNode: CellNode {
}

override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec {
displayNode.style.preferredSize.width = input.width
displayNode.style.preferredSize.height = 1
let spec = ASStackLayoutSpec()
spec.children = [displayNode, titleNode]
spec.direction = .vertical
spec.alignItems = .baselineFirst
let elements: [ASLayoutElement]

if imageNode.image == nil {
elements = [spec]
elements = [titleNode]
} else {
elements = [imageNode, spec]
elements = [imageNode, titleNode]
imageNode.hitTestSlop = UIEdgeInsets(top: -8, left: -8, bottom: -8, right: -20)
}

Expand Down
5 changes: 3 additions & 2 deletions FlowCryptUI/Cell Nodes/RecipientEmailsCellNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ final public class RecipientEmailsCellNode: CellNode {
}

private enum Constants {
static let sectionInset = UIEdgeInsets(top: 8, left: 8, bottom: 0, right: 8)
static let sectionInset = UIEdgeInsets(top: 8, left: 0, bottom: 0, right: 0)
static let minimumLineSpacing: CGFloat = 4
}

private var onAction: RecipientTap?

public lazy var collectionNode: ASCollectionNode = {
let layout = UICollectionViewFlowLayout()
let layout = LeftAlignedCollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumInteritemSpacing = 1
layout.minimumLineSpacing = Constants.minimumLineSpacing
Expand All @@ -42,6 +42,7 @@ final public class RecipientEmailsCellNode: CellNode {
super.init()
collectionNode.dataSource = self
collectionNode.delegate = self

automaticallyManagesSubnodes = true
}

Expand Down
36 changes: 36 additions & 0 deletions FlowCryptUI/Views/LeftAlignedCollectionViewFlowLayout.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// LeftAlignedCollectionViewFlowLayout.swift
// FlowCryptUI
//
// Created by Roma Sosnovsky on 21/10/21
// Copyright © 2017-present FlowCrypt a. s. All rights reserved.
//


import UIKit

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)

var leftMargin = sectionInset.left
var prevMaxY: CGFloat = -1.0

attributes?.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .cell else {
return
}

if layoutAttribute.frame.origin.y >= prevMaxY {
leftMargin = sectionInset.left
}

layoutAttribute.frame.origin.x = leftMargin

leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
prevMaxY = layoutAttribute.frame.maxY
}

return attributes
}
}