Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Any parsable URL will be added into the Draft body #1370

Merged
merged 7 commits into from
Mar 27, 2024
8 changes: 2 additions & 6 deletions Mail/Views/New Message/Attachments/AttachmentsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,8 @@ import SwiftUI
await worker.completeUploadedAttachments()
}

func processTextAttachment(_ attachment: TextAttachable?) async {
guard let attachment else {
return
}

await worker.processTextAttachment(attachment)
func processTextAttachments(_ attachments: [TextAttachable]) async {
await worker.processTextAttachments(attachments)
}

func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentUploadTask {
Expand Down
4 changes: 2 additions & 2 deletions Mail/Views/New Message/ComposeMessageIntentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ struct ComposeMessageIntentView: View, IntentViewable {
}

let composeMessageIntent: ComposeMessageIntent
var textAttachment: TextAttachable?
var textAttachments: [TextAttachable] = []
var attachments: [Attachable] = []

var body: some View {
Expand All @@ -50,7 +50,7 @@ struct ComposeMessageIntentView: View, IntentViewable {
mailboxManager: resolvedIntent.mailboxManager,
messageReply: resolvedIntent.messageReply,
attachments: attachments,
textAttachment: textAttachment
textAttachments: textAttachments
)
.environmentObject(resolvedIntent.mailboxManager)
} else {
Expand Down
9 changes: 4 additions & 5 deletions Mail/Views/New Message/ComposeMessageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ struct ComposeMessageView: View {
private let messageReply: MessageReply?
private let draftContentManager: DraftContentManager
private let mailboxManager: MailboxManager
private let textAttachment: TextAttachable?
private let textAttachments: [TextAttachable]

private var isSendButtonDisabled: Bool {
let disabledState = draft.identityId == nil
Expand All @@ -118,9 +118,10 @@ struct ComposeMessageView: View {
mailboxManager: MailboxManager,
messageReply: MessageReply? = nil,
attachments: [Attachable] = [],
textAttachment: TextAttachable? = nil
textAttachments: [TextAttachable] = []
PhilippeWeidmann marked this conversation as resolved.
Show resolved Hide resolved
) {
self.messageReply = messageReply
self.textAttachments = textAttachments

_draft = ObservedRealmObject(wrappedValue: draft)

Expand All @@ -131,8 +132,6 @@ struct ComposeMessageView: View {
)
draftContentManager = currentDraftContentManager

self.textAttachment = textAttachment

self.mailboxManager = mailboxManager
_attachmentsManager = StateObject(wrappedValue: AttachmentsManager(draftLocalUUID: draft.localUUID,
mailboxManager: mailboxManager))
Expand Down Expand Up @@ -224,7 +223,7 @@ struct ComposeMessageView: View {
currentSignature = try await draftContentManager.prepareCompleteDraft()

async let _ = attachmentsManager.completeUploadedAttachments()
async let _ = attachmentsManager.processTextAttachment(textAttachment)
async let _ = attachmentsManager.processTextAttachments(textAttachments)

isLoadingContent = false
} catch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,9 +370,20 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable {
await updateDelegate?.contentWillChange()
}

public func processTextAttachment(_ attachment: TextAttachable) async {
// process attachment
let textAttachment = await attachment.textAttachment
public func processTextAttachments(_ attachments: [TextAttachable]) async {
// Process all text attachments
let textAttachments = await attachments.concurrentMap { attachment in
await attachment.textAttachment
}

// Get first usable title
let anyUsableTitle = anyUsableTitle(in: textAttachments)

// Get all URLs
let allURLs = allURLs(in: textAttachments)

// Render all URLs as HTML code, if any after a minimalistic input sanitising
let formattedBodyUrls = formattedBodyUrls(allURLs: allURLs)

// mutate Draft
await backgroundRealm.execute { realm in
Expand All @@ -384,30 +395,55 @@ extension AttachmentsManagerWorker: AttachmentsManagerWorkable {
// Title if any usable
var modified = false
if draftInContext.subject.isEmpty,
let title = textAttachment.title {
draftInContext.subject = title
!anyUsableTitle.isEmpty {
draftInContext.subject = anyUsableTitle
modified = true
}

// Url if any after a minimalistic input sanitising
if let body = textAttachment.body,
let bodyUrl = URL(string: body),
!body.isEmpty {
draftInContext.body = "<div><a href=\"\(bodyUrl.absoluteString)\">"
+ bodyUrl.absoluteString
+ "</a></div>"
+ draftInContext.body
if !formattedBodyUrls.isEmpty {
draftInContext.body = formattedBodyUrls + draftInContext.body
modified = true
}

guard modified else {
return
}

realm.add(draftInContext, update: .modified)
}
}
}

private func anyUsableTitle(in textAttachments: [TextAttachment]) -> String {
textAttachments.first { $0.title?.isEmpty == false }?.title ?? ""
}

private func allURLs(in textAttachments: [TextAttachment]) -> [String] {
textAttachments.compactMap { attachment in
guard let body = attachment.body,
!body.isEmpty else {
return nil
}

return body
}
}

private func formattedBodyUrls(allURLs: [String]) -> String {
allURLs.reduce("") { partialResult, urlString in
guard let bodyUrl = URL(string: urlString) else {
return partialResult
}

let bodyAbsoluteUrl = bodyUrl.absoluteString
guard !bodyAbsoluteUrl.isEmpty else {
return partialResult
}

return partialResult + "<div><a href=\"\(bodyAbsoluteUrl)\">" + bodyAbsoluteUrl + "</a></div>"
}
}

@MainActor public func attachmentUploadTaskOrFinishedTask(for uuid: String) -> AttachmentUploadTask {
guard let attachment = attachmentUploadTask(for: uuid) else {
let finishedTask = AttachmentUploadTask()
Expand Down
22 changes: 18 additions & 4 deletions MailShareExtension/ComposeMessageWrapperView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,29 @@ struct ComposeMessageWrapperView: View {

static let typePropertyList = String(kUTTypePropertyList)

var textAttachment: TextAttachable? {
itemProviders.first { itemProvider in
/// All the Attachments that should directly provide URLs and title for a new Email Draft
var textAttachments: [TextAttachable] {
// All property list, result from JS execution in Safari
let propertyListItems = itemProviders.filter { itemProvider in
itemProvider.hasItemConformingToTypeIdentifier(Self.typePropertyList)
}

// All `.webloc` item providers, wrapped in a type that can read it on the fly
let weblocTextAttachments = itemProviders
.filter { $0.underlyingType == .isURL }
.compactMap { WeblocToTextAttachment(wrapping: $0) }

let allItems: [TextAttachable] = propertyListItems + weblocTextAttachments
adrien-coye marked this conversation as resolved.
Show resolved Hide resolved
return allItems
}

/// All the Attachments that should be uploaded as standalone files for a new Email Draft
var attachments: [Attachable] {
itemProviders.filter { itemProvider in
!itemProvider.hasItemConformingToTypeIdentifier(Self.typePropertyList)
let isPropertyList = itemProvider.hasItemConformingToTypeIdentifier(Self.typePropertyList)
let isUrlAsWebloc = itemProvider.underlyingType == .isURL

return !isPropertyList && !isUrlAsWebloc
}
}

Expand All @@ -57,7 +71,7 @@ struct ComposeMessageWrapperView: View {
} else if let mailboxManager = accountManager.currentMailboxManager {
ComposeMessageIntentView(
composeMessageIntent: .new(originMailboxManager: mailboxManager),
textAttachment: textAttachment,
textAttachments: textAttachments,
attachments: attachments
)
.environmentObject(mailboxManager)
Expand Down
59 changes: 59 additions & 0 deletions MailShareExtension/WeblocToTextAttachment.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
Infomaniak Mail - iOS App
Copyright (C) 2024 Infomaniak Network SA

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 <http://www.gnu.org/licenses/>.
*/

import Foundation
import MailCore

/// A wrapping type that can read an NSItemProvider that renders as a`.webloc` on the fly and provide the content thanks to the
/// `TextAttachable` protocol
struct WeblocToTextAttachment: TextAttachable {
let item: NSItemProvider

init?(wrapping item: NSItemProvider) {
guard item.underlyingType == .isURL else {
return nil
}

self.item = item
}

// MARK: TextAttachable protocol

var textAttachment: MailCore.TextAttachment {
get async {
guard let webloc = try? await item.writeToTemporaryURL() else {
return (nil, nil)
}

guard let weblocData = NSData(contentsOf: webloc.url) else {
return (nil, nil)
}

guard let parsedWebloc = try? PropertyListSerialization.propertyList(from: weblocData as Data,
options: [],
format: nil) as? NSDictionary else {
return (nil, nil)
}

let parsedURL = parsedWebloc["URL"] as? String

/// The `webloc` title is not useful for an email subject, discarding it
return TextAttachment(title: nil, body: parsedURL)
}
}
}
Loading