Commit 348cff0a authored by wieseoli's avatar wieseoli
Browse files

Merge branch '293-integrate-attachment-functionality-in-new-composeview' into 'dev'

Resolve "Integrate attachment functionality in new ComposeView"

Closes #293

See merge request !83
parents 2578aaa5 f441c888
......@@ -212,6 +212,9 @@
71DF08982421520D00162B74 /* EmailStringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71DF08972421520D00162B74 /* EmailStringExtensionTests.swift */; };
71DFE5BA240679E80042019C /* HeaderExtractionValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71DFE5B9240679E80042019C /* HeaderExtractionValues.swift */; };
786C2433142C638AE0605CD2 /* Pods_enzevalos_iphoneUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 434D0E988CDFB6D2D46C1EA8 /* Pods_enzevalos_iphoneUITests.framework */; };
7FE3F9D6260E76A40025340B /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE3F9D5260E76A40025340B /* ImagePicker.swift */; };
7FE3F9E9260F45600025340B /* AddAttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE3F9E8260F45600025340B /* AddAttachmentsView.swift */; };
7FE3F9FA260F467A0025340B /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FE3F9F9260F467A0025340B /* Attachment.swift */; };
971D404A2428C87E002FCD31 /* BadgeCaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 971D40492428C87E002FCD31 /* BadgeCaseView.swift */; };
97BDE0432429188500B0BF03 /* BadgeProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97BDE0422429188500B0BF03 /* BadgeProgressView.swift */; };
988C9C5D240D507A006213F0 /* UrlStringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 988C9C5C240D507A006213F0 /* UrlStringExtensionTests.swift */; };
......@@ -588,6 +591,9 @@
678942622430C40600C746D1 /* MailComparisonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailComparisonTests.swift; sourceTree = "<group>"; };
71DF08972421520D00162B74 /* EmailStringExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailStringExtensionTests.swift; sourceTree = "<group>"; };
71DFE5B9240679E80042019C /* HeaderExtractionValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderExtractionValues.swift; sourceTree = "<group>"; };
7FE3F9D5260E76A40025340B /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
7FE3F9E8260F45600025340B /* AddAttachmentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAttachmentsView.swift; sourceTree = "<group>"; };
7FE3F9F9260F467A0025340B /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
8EE6653EBD27D31263A36CA9 /* Pods-enzevalos_iphoneTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-enzevalos_iphoneTests.release.xcconfig"; path = "Target Support Files/Pods-enzevalos_iphoneTests/Pods-enzevalos_iphoneTests.release.xcconfig"; sourceTree = "<group>"; };
971D40492428C87E002FCD31 /* BadgeCaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeCaseView.swift; sourceTree = "<group>"; };
97BDE0422429188500B0BF03 /* BadgeProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeProgressView.swift; sourceTree = "<group>"; };
......@@ -1229,6 +1235,9 @@
47FA8EB1254D93D2006883D0 /* Compose */ = {
isa = PBXGroup;
children = (
7FE3F9E8260F45600025340B /* AddAttachmentsView.swift */,
7FE3F9F9260F467A0025340B /* Attachment.swift */,
7FE3F9D5260E76A40025340B /* ImagePicker.swift */,
47FA8EC2254D9E01006883D0 /* RecipientFieldModel.swift */,
47E04DD22559992A00189320 /* RecipientsModel.swift */,
3FB75DC825FFA77800919925 /* RecipientRowView.swift */,
......@@ -1308,6 +1317,13 @@
name = Frameworks;
sourceTree = "<group>";
};
7FE3F9C0260E18BA0025340B /* Recovered References */ = {
isa = PBXGroup;
children = (
);
name = "Recovered References";
sourceTree = "<group>";
};
82C71EC1E0E091561B645F91 /* Pods */ = {
isa = PBXGroup;
children = (
......@@ -1373,6 +1389,7 @@
A13526761D955BDF00D3BFE1 /* Products */,
78280F99990BFF65543B7F0B /* Frameworks */,
82C71EC1E0E091561B645F91 /* Pods */,
7FE3F9C0260E18BA0025340B /* Recovered References */,
);
sourceTree = "<group>";
};
......@@ -1909,6 +1926,7 @@
6789425F2430C3B300C746D1 /* MailComparison.swift in Sources */,
A114E4321FACB23000E40243 /* StringExtension.swift in Sources */,
472F398C1E2519C8009260FB /* CNContactExtension.swift in Sources */,
7FE3F9FA260F467A0025340B /* Attachment.swift in Sources */,
476406972416B54D00C7D426 /* InboxCoordinator.swift in Sources */,
0EF148082422572500B3C198 /* general-helpers.c in Sources */,
97BDE0432429188500B0BF03 /* BadgeProgressView.swift in Sources */,
......@@ -1931,6 +1949,7 @@
F14239C11F30A99C00998A83 /* QRCodeGenerator.swift in Sources */,
4764069C2416B54D00C7D426 /* VCSwiftUIView.swift in Sources */,
47BCAF26259CD52F0008FE4B /* PublicKeyListView.swift in Sources */,
7FE3F9D6260E76A40025340B /* ImagePicker.swift in Sources */,
478154A921FF3FF400A931EC /* Invitation.swift in Sources */,
4733B1E52527196100AB5600 /* PersistentDataProvider.swift in Sources */,
F1866C86201F707200B72453 /* EmailHelper.m in Sources */,
......@@ -1970,6 +1989,7 @@
47EABF0A241A9C8700774A93 /* AuthenticationViewModel.swift in Sources */,
47FA8EAC254D77DE006883D0 /* MailListView.swift in Sources */,
47C8225924379EAE005BCE73 /* AttPreview.swift in Sources */,
7FE3F9E9260F45600025340B /* AddAttachmentsView.swift in Sources */,
475B00341F7B9565006CDD41 /* Cryptography.swift in Sources */,
A1EB057C1D956838008659C1 /* MailHandler.swift in Sources */,
0EFEF0952417C0B400BB2FF7 /* CHelpers.swift in Sources */,
......
//
// AddAttachmentsView.swift
// enzevalos_iphone
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see https://www.gnu.org/licenses/.
//
import SwiftUI
/// A view that enables uploading an previewing files and pictures as attachments.
struct AddAttachmentsView: View {
@ObservedObject var model: ComposeModel
@State private var attachFile = false
@State private var imageAttachment: UIImage?
// Two sheet states possible: full screen preview OR image picker
@State private var sheetState: SheetState?
// TODO: image import and preview are currently not fully supported
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
// both upload buttons
VStack {
attachFileButton
Spacer()
imageUploadButton
.padding(.bottom, 3)
}
filePreviews
}
.padding(4)
// sheet used for image import OR full screen attachment preview
.frame(maxHeight: 141)
}
.sheet(item: $sheetState) { state in
switch state {
case .imagePicker:
ImagePicker(image: $imageAttachment)
case let .fullScreenPreview(attachment):
Text(attachment.myName)
.font(Font.body.weight(.semibold))
QuickLookView(name: attachment.myName,
data: attachment.myData,
shallDL: false)
.aspectRatio(100/141, contentMode: .fit)
}
}
}
/// a view that contains the upload button for files
var attachFileButton: some View {
Button {
attachFile = true
} label: {
Image(systemName: "doc.badge.plus").font(.system(size: 50))
}
// file import when button is pressed
.fileImporter(isPresented: $attachFile, allowedContentTypes: [.plainText, .image, .pdf]) {
res in
do {
// get fileURL of selected file
let fileURL = try res.get()
// get fileName from URL of that file
let fileName = fileURL.lastPathComponent
// get file data
let fileData = try Data(contentsOf: fileURL)
// create Attachment and add in to the attachments list
let newAttachment = Attachment(myName: fileName, myData: fileData)
model.attachments.append(newAttachment)
} catch {
// Error while loading file
print("Error while importing file.")
}
}
}
/// a view that contains the upload button for pictures
var imageUploadButton: some View {
Button {
sheetState = .imagePicker
} label: {
// try to match SF Symbol "doc.badge.plus" with "photo"
ZStack(alignment: .bottomLeading) {
Image(systemName: "photo").font(.system(size: 50))
Image(systemName: "plus")
.font(Font.system(size: 19, weight: .bold))
.foregroundColor(Color(.systemBackground))
.padding(3)
.background(Circle().fill(Color.accentColor))
.padding(3)
.background(Circle().fill(Color(.systemBackground)))
.offset(x: -8, y: 8)
}
}
}
/// a view that contains several file previews together with their delete buttons
var filePreviews: some View {
ForEach(model.attachments.reversed()) { attachment in
ZStack {
// file preview using Quicklook
// shallDL has to be true here, because QuickLookView will download
// the file into the Documents Directory for us
// it then uses that DD file to create a preview of the content
QuickLookView(name: attachment.myName, data: attachment.myData, shallDL: true)
.padding(3)
.background(RoundedRectangle(cornerRadius: 5)
.stroke(Color.secondary, lineWidth: 2))
// a window that disables the interaction with QuickLookView
Button {
sheetState = .fullScreenPreview(attachment)
} label: {
Rectangle().fill(Color(.systemBackground).opacity(0.1))
}
VStack(alignment: .trailing) {
// delete button in upper right corner
Button {
// remove from attachments list and remove copy from Documents Directory
// to keep DD clean
if let deleteIndex = model.attachments.firstIndex(of: attachment) {
model.attachments[deleteIndex].removeFileFromDocumentsDirectory()
model.attachments.remove(at: deleteIndex)
}
} label: {
Image(systemName: "xmark").font(.system(size: 20))
}
.padding(5)
Spacer()
// display file name and size
Text(attachment.myName + ", " + attachment.countData())
.lineLimit(1)
.font(.caption)
.foregroundColor(.secondary)
.truncationMode(.middle)
.frame(maxWidth: .infinity)
.padding(3)
.background(RoundedRectangle(cornerRadius: 5)
.fill(Color(.systemBackground).opacity(0.8)))
}
.frame(maxWidth: .infinity)
}
.frame(width: 100)
}
}
/// States to control several sheet views
enum SheetState: Identifiable {
case imagePicker // user presses imageUploadButton
case fullScreenPreview(Attachment) // user taps on a file preview
var id: Int {
switch self {
case .imagePicker: return 0
case .fullScreenPreview: return 1
}
}
}
}
//
// Attachment.swift
// enzevalos_iphone
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see https://www.gnu.org/licenses/.
//
import Foundation
struct Attachment: DisplayAttachment, Identifiable, Equatable {
// DisplayAttachment
var myName: String
var myData: Data
// Identifiable
var id = UUID()
/// a func that removes the calling attachment`s copy from the Documents Directory
func removeFileFromDocumentsDirectory() {
/// a func that returns the URL of the Documents Directory
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
let wholePath = getDocumentsDirectory().appendingPathComponent(self.myName)
do {
try FileManager.default.removeItem(at: wholePath)
} catch let error as NSError {
print("Error: \(error)")
}
}
/// a func that returns the size of the calling attachment file as a string
func countData() -> String {
// size in byte
var sizeOfData = Double(self.myData.count)
// smaller than 1KB
if sizeOfData < 1000 {
return String(sizeOfData) + " bytes"
}
//smaller than 1MB
else if sizeOfData < 1000000 {
sizeOfData = round(sizeOfData/1000)
return String(Int(sizeOfData)) + " KB"
}
// everything bigger than 1MB
else {
sizeOfData = round(sizeOfData/1000000)
return String(Int(sizeOfData)) + " MB"
}
}
}
......@@ -37,8 +37,19 @@ struct ComposeViewHeader: View {
// Send button, disabled if no recipients are set.
Button("Send") {
model.sendMail()
presentationMode.wrappedValue.dismiss()
// trigger an alert to attach files
// user can then decide in the alert if he wants to
// - resume sending
// - quit sending and edit again
if model.mentionsAttachments && model.attachments == [] {
model.showAttachmentAlert = true
model.resumeSend = false
}
if model.resumeSend {
model.sendMail()
presentationMode.wrappedValue.dismiss()
}
model.resumeSend = true
}
.disabled(model.recipientsModel.hasNoRecipients)
}
......
......@@ -25,8 +25,12 @@ import Foundation
class ComposeModel: ObservableObject {
@Published var subject = ""
@Published var body = ""
@Published var attachments: [Attachment] = []
@Published var encryptionOn = true
@Published var recipientsModel: RecipientsModel = RecipientsModel()
// properties for the attachment check/reminder
@Published var showAttachmentAlert = false
@Published var resumeSend = true
init(preData: PreMailData?) {
if let preData = preData {
......@@ -39,6 +43,22 @@ class ComposeModel: ObservableObject {
recipientsModel.parentComposeModel = self
}
/// computed property checks whether an attachment is mentioned in the mail (subject or body)
var mentionsAttachments: Bool {
let germanWords = ["Anhang", "anhang", "Angehängt", "angehängt", "Angehangen", "angehangen", "Anhänge", "anhänge"]
let englishWords = ["Attachment", "attachment", "Attachments", "attachments", "Attached", "attached", "Attach", "attach"]
// TODO: more languages?
let allWords = germanWords + englishWords
return allWords.contains(where: self.body.contains) || allWords.contains(where: self.subject.contains)
}
/// a func that deletes all the copies of the attachment files in the Documents Directory to keep it clean
func removeAttachmentCopiesFromDocumentsDirectory() {
for current in attachments {
current.removeFileFromDocumentsDirectory()
}
}
// TODO: Add security state functionality
/// Generates mail and sends it.
......@@ -61,14 +81,27 @@ class ComposeModel: ObservableObject {
///
/// - Returns: Outgoing mail filled out with relevant information from RecipientsModel.
private func generateMail() -> OutgoingMail {
OutgoingMail(toAddresses: recipientsModel.toEMails,
// before sending the mail we need to converted all attachments to MCOAttachments
var convertedAttachments: [MCOAttachment] = []
for current in attachments{
do {
if let newMCOAttachment = MCOAttachment.init(data: current.myData, filename: current.myName) {
convertedAttachments.append(newMCOAttachment)
print("Converted and attached file")
}
}
}
// TODO: something seems to go wrong with sending the attachments
// has to be fixed in the future
return OutgoingMail(toAddresses: recipientsModel.toEMails,
ccAddresses: recipientsModel.ccEMails,
bccAddresses: recipientsModel.bccEMails,
subject: subject,
textContent: body,
htmlContent: nil,
textparts: 1,
sendEncryptedIfPossible: encryptionOn,
attachments: [])
sendEncryptedIfPossible: !encryptionOn,
attachments: convertedAttachments)
}
}
......@@ -22,8 +22,13 @@ import SwiftUI
/// A view used to compose and send an email.
struct ComposeView: View {
@State private var cc = ""
@Environment(\.presentationMode) var presentationMode
@ObservedObject var model: ComposeModel
// properties regarding the RecipientFields
@State private var cc = ""
// properties regarding attachments
// used to control the floating action button
@State private var showAttachments = false
/// - Parameter preData: Data of email to reply to or forward.
init(preData: PreMailData? = nil) {
......@@ -58,12 +63,70 @@ struct ComposeView: View {
Divider()
// Email body
TextEditor(text: $model.body)
// Email body with attachment floating action button
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)) {
TextEditor(text: $model.body)
// floating action button
Button (
action: {showAttachments.toggle()},
label: {
Image(systemName: "paperclip")
}
)
.frame(alignment: .topLeading)
}
// optional upload attachments section
if showAttachments {
VStack {
Divider()
AddAttachmentsView(model: model)
}
.transition(.move(edge: .bottom))
}
}
// show this alert when user sends the mail without attachments,
// but mentions them in the mail
.alert(isPresented: $model.showAttachmentAlert) {
AttachmentAlert
}
.padding()
.animation(.easeInOut)
.ignoresSafeArea(edges: /*@START_MENU_TOKEN@*/.bottom/*@END_MENU_TOKEN@*/)
.onDisappear{
// when the ComposeView is closed, we make sure to
// clean the Documents Directory from all the remaining
// copies of attachment files
model.removeAttachmentCopiesFromDocumentsDirectory()
}
}
}
/// an alert which is shown when the user mentions attachments in the mail, but sends it without attaching anything
var AttachmentAlert: Alert {
Alert(
title: Text("Attachment.Alert.Title"),
message: Text("Attachment.Alert.Text"),
primaryButton: .destructive(Text("Attachment.Alert.PrimaryButton").foregroundColor(Color.blue)) {
// send the mail anyway
model.sendMail()
// remove the attachments from the Documents Directory
// to keep the DD clean
model.removeAttachmentCopiesFromDocumentsDirectory()
presentationMode.wrappedValue.dismiss()
},
secondaryButton: .destructive(Text("Attachment.Alert.SecondaryButton").foregroundColor(Color.blue)) {
// quit sending, enable further editing
}
)
}
/// a func that deletes all the copies of the attachment files in the Documents Directory to keep it clean
func removeAttachmentCopiesFromDocumentsDirectory() {
for current in model.attachments {
current.removeFileFromDocumentsDirectory()
}
}
}
......
//
// ImagePicker.swift
// enzevalos_iphone
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see https://www.gnu.org/licenses/.
//
/// This file is a SwiftUI wrapper for UIImagePickerController
/// it can be used it to attach pictures to a mail and preview them
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode) var presentationMode
@Binding var image: UIImage?
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let uiImage = info[.originalImage] as? UIImage {
parent.image = uiImage
}
parent.presentationMode.wrappedValue.dismiss()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
......@@ -54,6 +54,7 @@ struct InboxView: View {
}
}
private var mailListView: some View {
MailListView(folderPath: folderPath, folderName: folderName, refreshModel: refreshModel)
.environment(\.managedObjectContext,
......
......@@ -30,6 +30,10 @@
"AddressRecord.lastHeardFrom.Never" = "noch nie";
"Attach" = "Anhängen";
"Attachment" = "Anhang";
"Attachment.Alert.Title" = "Willst du diese E-Mail wirklich absenden";
"Attachment.Alert.Text" = "In deiner E-Mail erwähnst du Anhänge, aber der Anhang ist momentan leer.";
"Attachment.Alert.PrimaryButton" = "Senden";
"Attachment.Alert.SecondaryButton" = "E-Mail bearbeiten";
"Authentification" = "Authentifizierung";
"Back" = "Zurück";
"Bcc" = "Bcc";
......
......@@ -30,6 +30,10 @@
"AddressRecord.lastHeardFrom.Never" = "never";
"Attach" = "Attach";
"Attachment" = "Attachment";
"Attachment.Alert.Title" = "Are you sure you want to send this mail?";
"Attachment.Alert.Text" = "In your mail you mentioned attachments, but you did not attach any.";
"Attachment.Alert.PrimaryButton" = "Send";
"Attachment.Alert.SecondaryButton" = "Edit Mail";
"Authentification" = "Authentification";
"Back" = "Back";
"Bcc" = "Bcc";
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or