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
16 changes: 16 additions & 0 deletions FlowCrypt.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
2155E9EF26E3628C008FB033 /* Refreshable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2155E9EE26E3628C008FB033 /* Refreshable.swift */; };
215897E8267A553300423694 /* FilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 215897E7267A553200423694 /* FilesManager.swift */; };
21594C9626F1DBA900BE654C /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 21594C9526F1DBA900BE654C /* data.txt */; };
21623D1826FA860700A11B9A /* PhotosManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21623D1726FA860600A11B9A /* PhotosManager.swift */; };
21750D7D26C6AFA6007E6A6F /* SetupEKMKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21750D7C26C6AFA6007E6A6F /* SetupEKMKeyViewController.swift */; };
21750D7F26C6C1E3007E6A6F /* SetupCreatePassphraseAbstractViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21750D7E26C6C1E3007E6A6F /* SetupCreatePassphraseAbstractViewController.swift */; };
2196A2202684B9BE001B9E00 /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2196A21F2684B9BE001B9E00 /* URLExtension.swift */; };
Expand All @@ -36,6 +37,7 @@
21EA3B592656611D00691848 /* OrganisationalRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21EA3B15265647C400691848 /* OrganisationalRule.swift */; };
21EFF61F265A5C6700AB0B71 /* WKDURLsApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21EFF61E265A5C6700AB0B71 /* WKDURLsApi.swift */; };
21F836B62652A26B00B2448C /* DataExntensions+ZBase32Encoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21F836B52652A26B00B2448C /* DataExntensions+ZBase32Encoding.swift */; };
21FEE26626FDD91A00E3783F /* ComposeMessageAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21FEE26526FDD91A00E3783F /* ComposeMessageAttachment.swift */; };
32DCA00224982EDA88D69C6E /* AppErr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA4B11D4531B3B04D01D1 /* AppErr.swift */; };
32DCA04CA0DAB79C39514782 /* CoreTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCAC732B988D9704658812 /* CoreTypes.swift */; };
32DCA1414EEA727B86C337D5 /* Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA0C3D34A69851A238E87 /* Core.swift */; };
Expand Down Expand Up @@ -407,6 +409,7 @@
2155E9EE26E3628C008FB033 /* Refreshable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Refreshable.swift; sourceTree = "<group>"; };
215897E7267A553200423694 /* FilesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesManager.swift; sourceTree = "<group>"; };
21594C9526F1DBA900BE654C /* data.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = data.txt; sourceTree = "<group>"; };
21623D1726FA860600A11B9A /* PhotosManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosManager.swift; sourceTree = "<group>"; };
21750D7C26C6AFA6007E6A6F /* SetupEKMKeyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupEKMKeyViewController.swift; sourceTree = "<group>"; };
21750D7E26C6C1E3007E6A6F /* SetupCreatePassphraseAbstractViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupCreatePassphraseAbstractViewController.swift; sourceTree = "<group>"; };
2196A21F2684B9BE001B9E00 /* URLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtension.swift; sourceTree = "<group>"; };
Expand All @@ -425,6 +428,7 @@
21F836B52652A26B00B2448C /* DataExntensions+ZBase32Encoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataExntensions+ZBase32Encoding.swift"; sourceTree = "<group>"; };
21F836CB2652A38700B2448C /* ZBase32EncodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZBase32EncodingTests.swift; sourceTree = "<group>"; };
21F836D22652A46E00B2448C /* WKDURLsConstructorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKDURLsConstructorTests.swift; sourceTree = "<group>"; };
21FEE26526FDD91A00E3783F /* ComposeMessageAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageAttachment.swift; sourceTree = "<group>"; };
27D857C43583281B45F427F8 /* Pods-FlowCryptUI.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptUI.release.xcconfig"; path = "Target Support Files/Pods-FlowCryptUI/Pods-FlowCryptUI.release.xcconfig"; sourceTree = "<group>"; };
2B4C2647A021692455F9DFAD /* Pods-FlowCryptAppTests.enterprise.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptAppTests.enterprise.xcconfig"; path = "Target Support Files/Pods-FlowCryptAppTests/Pods-FlowCryptAppTests.enterprise.xcconfig"; sourceTree = "<group>"; };
32DCA058652FD4616FB04FB6 /* SequenceExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SequenceExtensions.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -868,6 +872,14 @@
path = FilesManager;
sourceTree = "<group>";
};
21623D1926FA860E00A11B9A /* PhotosManager */ = {
isa = PBXGroup;
children = (
21623D1726FA860600A11B9A /* PhotosManager.swift */,
);
path = PhotosManager;
sourceTree = "<group>";
};
21CE25D32650034500ADFF4B /* WKDURLs */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1300,6 +1312,7 @@
children = (
9F6F3BEC26ADF5DE005BD9C6 /* ComposeMessageService.swift */,
9F6F3BED26ADF5DE005BD9C6 /* ComposeMessageError.swift */,
21FEE26526FDD91A00E3783F /* ComposeMessageAttachment.swift */,
);
path = "Compose Message Service";
sourceTree = "<group>";
Expand Down Expand Up @@ -1624,6 +1637,7 @@
C132B9C61EC2DCC000763715 /* Functionality */ = {
isa = PBXGroup;
children = (
21623D1926FA860E00A11B9A /* PhotosManager */,
215897E6267A551300423694 /* FilesManager */,
21CE25D32650034500ADFF4B /* WKDURLs */,
A370EAB6238697E000685215 /* Pgp */,
Expand Down Expand Up @@ -2540,6 +2554,7 @@
9FB22CE425715D3E0026EE64 /* GmailServiceErrorHandler.swift in Sources */,
9F4163E6266520B600106194 /* CommonNodesInputs.swift in Sources */,
04B472951ECE29F600B8266F /* MyMenuViewController.swift in Sources */,
21623D1826FA860700A11B9A /* PhotosManager.swift in Sources */,
C132B9B41EC2DBD800763715 /* AppDelegate.swift in Sources */,
21489B7C267CBA0E00BDE4AC /* ClientConfiguration.swift in Sources */,
9F976592267E19880058419D /* TestData.swift in Sources */,
Expand Down Expand Up @@ -2681,6 +2696,7 @@
32DCAF9DA9EC47798DF8BB73 /* SignInViewController.swift in Sources */,
21489B78267CB42400BDE4AC /* ClientConfigurationProvider.swift in Sources */,
9FF0671C25520D9D00FCC9E6 /* MailProvider.swift in Sources */,
21FEE26626FDD91A00E3783F /* ComposeMessageAttachment.swift in Sources */,
9FE743072347AA54005E2DBB /* MainNavigationController.swift in Sources */,
9F5C2A8B257E6C4900DE9B4B /* ImapError.swift in Sources */,
32DCA594BD65DE3AF94569F3 /* ComposeViewController.swift in Sources */,
Expand Down
186 changes: 163 additions & 23 deletions FlowCrypt/Controllers/Compose/ComposeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ final class ComposeViewController: TableNodeViewController {
private let notificationCenter: NotificationCenter
private let decorator: ComposeViewDecorator
private let contactsService: ContactsServiceType
private let filesManager: FilesManagerType
private let photosManager: PhotosManagerType

private let searchThrottler = Throttler(seconds: 1)
private let cloudContactProvider: CloudContactsProvider
Expand All @@ -55,7 +57,9 @@ final class ComposeViewController: TableNodeViewController {
cloudContactProvider: CloudContactsProvider = UserContactsProvider(),
userDefaults: UserDefaults = .standard,
contactsService: ContactsServiceType = ContactsService(),
composeMessageService: ComposeMessageService = ComposeMessageService()
composeMessageService: ComposeMessageService = ComposeMessageService(),
filesManager: FilesManagerType = FilesManager(),
photosManager: PhotosManagerType = PhotosManager()
) {
self.email = email
self.notificationCenter = notificationCenter
Expand All @@ -65,6 +69,8 @@ final class ComposeViewController: TableNodeViewController {
self.userDefaults = userDefaults
self.contactsService = contactsService
self.composeMessageService = composeMessageService
self.filesManager = filesManager
self.photosManager = photosManager
self.contextToSend.subject = input.subject
super.init(node: TableNode())
}
Expand Down Expand Up @@ -184,8 +190,7 @@ extension ComposeViewController {
}

@objc private func handleAttachTap() {
#warning("ToDo")
showToast("Attachments not implemented yet")
openAttachmentsInputSourcesSheet()
}

@objc private func handleSendTap() {
Expand All @@ -198,15 +203,13 @@ extension ComposeViewController {
extension ComposeViewController {
private func sendMessage() {
view.endEditing(true)

showSpinner("sending_title".localized)
navigationItem.rightBarButtonItem?.isEnabled = false

composeMessageService.validateMessage(
input: input,
contextToSend: contextToSend,
email: email,
atts: []
email: email
)
.publisher
.flatMap(composeMessageService.encryptAndSend)
Expand Down Expand Up @@ -243,7 +246,7 @@ extension ComposeViewController {

extension ComposeViewController: ASTableDelegate, ASTableDataSource {
func numberOfSections(in _: ASTableNode) -> Int {
2
3
}

func tableNode(_: ASTableNode, numberOfRowsInSection section: Int) -> Int {
Expand All @@ -252,6 +255,8 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource {
return RecipientParts.allCases.count
case (.main, 1):
return ComposeParts.allCases.count
case (.main, 2):
return contextToSend.attachments.count
case (.searchEmails, 0):
return RecipientParts.allCases.count
case let (.searchEmails(emails), 1):
Expand Down Expand Up @@ -285,6 +290,11 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource {
case .text: return self.textNode(with: nodeHeight)
case .subjectDivider: return DividerCellNode()
}
case (.main, 2):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the whole tableNode should be in some sort of decorator class? It looks very UI-heavy with not much business logic. What do you think @Kharchevskyi ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If so, then that would apply to the whole extension ComposeViewController: ASTableDelegate, ASTableDataSource { section

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, we may create DataSource entity for this screen, @Kharchevskyi let's discuss that when it's convenient

guard !self.contextToSend.attachments.isEmpty else {
return ASCellNode()
}
return self.attachmentNode(for: indexPath.row)
case let (.searchEmails(emails), 1):
return InfoCellNode(input: self.decorator.styledRecipientInfo(with: emails[indexPath.row]))
default:
Expand Down Expand Up @@ -328,21 +338,17 @@ extension ComposeViewController {
}

private func textNode(with nodeHeight: CGFloat) -> ASCellNode {
let textFieldHeight = decorator.styledTextFieldInput(with: "").height
let dividerHeight: CGFloat = 1
let preferredHeight = nodeHeight - 2 * (textFieldHeight + dividerHeight)

return TextViewCellNode(
decorator.styledTextViewInput(with: preferredHeight)
) { [weak self] event in
guard case let .didEndEditing(text) = event else { return }
self?.contextToSend.message = text?.string
}
.then {
guard self.input.isReply else { return }
$0.textView.attributedText = self.decorator.styledReplyQuote(with: self.input)
$0.becomeFirstResponder()
}
decorator.styledTextViewInput(with: 40),
action: { [weak self] event in
guard case let .didEndEditing(text) = event else { return }
self?.contextToSend.message = text?.string
})
.then {
guard self.input.isReply else { return }
$0.textView.attributedText = self.decorator.styledReplyQuote(with: self.input)
$0.becomeFirstResponder()
}
}

private func recipientsNode() -> RecipientEmailsCellNode {
Expand All @@ -357,7 +363,7 @@ extension ComposeViewController {

private func recipientInput() -> TextFieldCellNode {
TextFieldCellNode(
input: decorator.styledTextFieldInput(with: "compose_recipient".localized)
input: decorator.styledTextFieldInput(with: "compose_recipient".localized, keyboardType: .emailAddress)
) { [weak self] action in
self?.handleTextFieldAction(with: action)
}
Expand All @@ -375,6 +381,14 @@ extension ComposeViewController {
}
}
}

private func attachmentNode(for index: Int) -> ASCellNode {
AttachmentNode(
input: .init(
composeAttachment: contextToSend.attachments[index]
)
)
}
}

// MARK: - Recipients Input
Expand Down Expand Up @@ -633,6 +647,132 @@ extension ComposeViewController {
}
}

// MARK: - UIDocumentPickerDelegate
extension ComposeViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let fileUrl = urls.first,
let attachment = ComposeMessageAttachment(fileURL: fileUrl)
else {
showAlert(message: "files_picking_files_error_message".localized)
return
}
appendAttachmentIfAllowed(attachment)
node.reloadSections(IndexSet(integer: 2), with: .automatic)
}
}

// MARK: - UIImagePickerControllerDelegate & UINavigationControllerDelegate
extension ComposeViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
) {
picker.dismiss(animated: true, completion: nil)

let attachment: ComposeMessageAttachment?
switch picker.sourceType {
case .camera:
attachment = ComposeMessageAttachment(cameraSourceMediaInfo: info)
case .photoLibrary:
attachment = ComposeMessageAttachment(librarySourceMediaInfo: info)
default: fatalError("No other image picker's sources should be used")
}
guard let attachment = attachment else {
showAlert(message: "files_picking_photos_error_message".localized)
return
}
appendAttachmentIfAllowed(attachment)
node.reloadSections(IndexSet(integer: 2), with: .automatic)
}

private func appendAttachmentIfAllowed(_ attachment: ComposeMessageAttachment) {
let totalSize = contextToSend.attachments.reduce(0, { $0 + $1.size })
if totalSize > GeneralConstants.Global.attachmentSizeLimit {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be totalSize + attachment.size > GeneralConstants.Global.attachmentSizeLimit ?

showToast("files_picking_size_error_message".localized)
} else {
contextToSend.attachments.append(attachment)
}
}
}

// MARK: - Attachments sheet handling
extension ComposeViewController {
private func openAttachmentsInputSourcesSheet() {
let alert = UIAlertController(
title: "files_picking_select_input_source_title".localized,
message: nil, preferredStyle: .actionSheet
)
alert.addAction(
UIAlertAction(
title: "files_picking_camera_input_source".localized,
style: .default,
handler: { [weak self] _ in
guard let self = self else { return }
self.photosManager.selectPhoto(source: .camera, from: self)
.sinkFuture(
receiveValue: {},
receiveError: { _ in
self.showNoAccessToPhotosAlert()
}
)
.store(in: &self.cancellable)
}
)
)
alert.addAction(
UIAlertAction(
title: "files_picking_photo_library_source".localized,
style: .default,
handler: { [weak self] _ in
guard let self = self else { return }
self.photosManager.selectPhoto(source: .photoLibrary, from: self)
.sinkFuture(
receiveValue: {},
receiveError: { _ in
self.showNoAccessToPhotosAlert()
}
)
.store(in: &self.cancellable)
}
)
)
alert.addAction(
UIAlertAction(
title: "files_picking_files_source".localized,
style: .default,
handler: { [weak self] _ in
guard let self = self else { return }
self.filesManager.selectFromFilesApp(from: self)
}
)
)
alert.addAction(UIAlertAction(title: "cancel".localized, style: .cancel))
present(alert, animated: true, completion: nil)
}

private func showNoAccessToPhotosAlert() {
let alert = UIAlertController(
title: "files_picking_no_library_access_error_title".localized,
message: "files_picking_no_library_access_error_message".localized,
preferredStyle: .alert
)
let okAction = UIAlertAction(
title: "OK",
style: .cancel
) { _ in }
let settingsAction = UIAlertAction(
title: "setttings".localized,
style: .default
) { _ in
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
}
alert.addAction(okAction)
alert.addAction(settingsAction)

present(alert, animated: true, completion: nil)
}
}

// temporary disable search contacts
// https://github.com/FlowCrypt/flowcrypt-ios/issues/217
extension ComposeViewController {
Expand All @@ -650,7 +790,7 @@ extension ComposeViewController {
style: .default
) { _ in }
let cancelAction = UIAlertAction(
title: "Cancel",
title: "cancel".localized,
style: .destructive
) { _ in }
alert.addAction(okAction)
Expand Down
17 changes: 15 additions & 2 deletions FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ struct ComposeViewDecorator {
)
}

func styledTextFieldInput(with text: String) -> TextFieldCellNode.Input {
func styledTextFieldInput(with text: String, keyboardType: UIKeyboardType = .default) -> TextFieldCellNode.Input {
TextFieldCellNode.Input(
placeholder: text.localized.attributed(
.regular(17),
Expand All @@ -43,7 +43,8 @@ struct ComposeViewDecorator {
textInsets: -8,
textAlignment: .left,
height: 40,
width: UIScreen.main.bounds.width
width: UIScreen.main.bounds.width,
keyboardType: keyboardType
)
}

Expand Down Expand Up @@ -190,3 +191,15 @@ extension RecipientEmailsCellNode.Input {
)
}
}

// MARK: - AttachmentNode.Input
extension AttachmentNode.Input {
init(composeAttachment: ComposeMessageAttachment) {
self.init(
name: composeAttachment.name
.attributed(.regular(18), color: .textColor, alignment: .left),
size: "\(composeAttachment.size)"
.attributed(.medium(12), color: .textColor, alignment: .left)
)
}
}
Loading