diff --git a/Mail/Helpers/OldRichTextEditor.swift b/Mail/Helpers/OldRichTextEditor.swift
deleted file mode 100644
index 62b61d0316..0000000000
--- a/Mail/Helpers/OldRichTextEditor.swift
+++ /dev/null
@@ -1,510 +0,0 @@
-/*
- Infomaniak Mail - iOS App
- Copyright (C) 2022 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 .
- */
-
-import InfomaniakCore
-import InfomaniakCoreUI
-import InfomaniakDI
-import InfomaniakRichEditor
-import MailCore
-import MailCoreUI
-import MailResources
-import SQRichTextEditor
-import SwiftUI
-import WebKit
-
-struct OldRichTextEditor: UIViewRepresentable {
- @State private var mustUpdateBody = false
-
- @Binding var model: RichTextEditorModel
- @Binding var body: String
- @Binding var isShowingCamera: Bool
- @Binding var isShowingFileSelection: Bool
- @Binding var isShowingPhotoLibrary: Bool
- @Binding var becomeFirstResponder: Bool
- @Binding var isShowingAIPrompt: Bool
- @Binding var isShowingAlert: NewMessageAlert?
-
- let blockRemoteContent: Bool
-
- init(model: Binding,
- body: Binding,
- isShowingCamera: Binding,
- isShowingFileSelection: Binding,
- isShowingPhotoLibrary: Binding,
- becomeFirstResponder: Binding,
- isShowingAIPrompt: Binding,
- isShowingAlert: Binding,
- blockRemoteContent: Bool) {
- _model = model
- _body = body
- _isShowingCamera = isShowingCamera
- _isShowingFileSelection = isShowingFileSelection
- _isShowingPhotoLibrary = isShowingPhotoLibrary
- _becomeFirstResponder = becomeFirstResponder
- _isShowingAIPrompt = isShowingAIPrompt
- _isShowingAlert = isShowingAlert
- self.blockRemoteContent = blockRemoteContent
- }
-
- class Coordinator: SQTextEditorDelegate {
- var parent: OldRichTextEditor
-
- init(_ parent: OldRichTextEditor) {
- self.parent = parent
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(requireBodyUpdate),
- name: Notification.Name.updateComposeMessageBody,
- object: nil
- )
- }
-
- @MainActor
- func insertBody(editor: SQTextEditorView) async throws {
- guard let editor = (editor as? MailEditorView) else { throw MailError.unknownError }
- try await insertBody(editor: editor)
- }
-
- @MainActor
- func insertBody(editor: MailEditorView) async throws {
- try await editor.contentBlocker.setRemoteContentBlocked(parent.blockRemoteContent)
- editor.clear()
- try await editor.insertRawHTML(parent.body)
- editor.moveCursorToStart()
- editor.webView.scrollView.isScrollEnabled = false
- parent.model.height = CGFloat(editor.contentHeight)
- }
-
- func editorDidLoad(_ editor: SQTextEditorView) {
- Task {
- do {
- try await insertBody(editor: editor)
- } catch {
- print("Failed to load editor: \(error)")
- }
- }
- }
-
- func editor(_ editor: SQTextEditorView, contentHeightDidChange height: Int) {
- parent.model.height = CGFloat(height)
- }
-
- func editor(_ editor: SQTextEditorView, cursorPositionDidChange position: SQEditorCursorPosition) {
- let newCursorPosition = CGFloat(position.bottom) + 20
- if parent.model.cursorPosition != newCursorPosition {
- parent.model.cursorPosition = newCursorPosition
- }
- }
-
- func editor(_ editor: SQTextEditorView, selectedTextAttributeDidChange attribute: SQTextAttribute) {
- if let mailEditor = editor as? MailEditorView {
- mailEditor.updateToolbarItems(style: mailEditor.toolbarStyle)
- }
- }
-
- func editorContentChanged(_ editor: SQTextEditorView, content: String) {
- var parentBody = parent.body.trimmingCharacters(in: .whitespacesAndNewlines)
- parentBody = parentBody.replacingOccurrences(of: "\r", with: "")
- if parentBody != content {
- parent.body = content
- }
- }
-
- @objc func requireBodyUpdate() {
- parent.mustUpdateBody = true
- }
- }
-
- func makeCoordinator() -> Coordinator {
- return Coordinator(self)
- }
-
- func makeUIView(context: Context) -> MailEditorView {
- let richTextEditor = MailEditorView(
- isShowingCamera: $isShowingCamera,
- isShowingFileSelection: $isShowingFileSelection,
- isShowingPhotoLibrary: $isShowingPhotoLibrary,
- isShowingAIPrompt: $isShowingAIPrompt,
- isShowingAlert: $isShowingAlert
- )
- richTextEditor.delegate = context.coordinator
- return richTextEditor
- }
-
- func updateUIView(_ uiView: MailEditorView, context: Context) {
- if becomeFirstResponder {
- DispatchQueue.main.async {
- uiView.setBecomeFirstResponder()
- becomeFirstResponder = false
- }
- }
- if mustUpdateBody {
- Task {
- mustUpdateBody = false
- try await context.coordinator.insertBody(editor: uiView)
- }
- }
- }
-
- static func dismantleUIView(_ uiView: MailEditorView, coordinator: Coordinator) {
- uiView.webView.configuration.userContentController.removeAllScriptMessageHandlers()
- }
-}
-
-extension SQTextEditorView {
- func insertHtml(_ html: String) async throws {
- return try await withCheckedThrowingContinuation { continuation in
- insertHTML(html) { error in
- if let error {
- continuation.resume(throwing: error)
- } else {
- continuation.resume()
- }
- }
- }
- }
-}
-
-struct RichTextEditorModel {
- var cursorPosition: CGFloat = 0
- var height: CGFloat = 0
-}
-
-final class MailEditorView: SQTextEditorView {
- @LazyInjectService var matomo: MatomoUtils
-
- lazy var toolbar = getToolbar()
- var isShowingCamera: Binding
- var isShowingFileSelection: Binding
- var isShowingPhotoLibrary: Binding
- var isShowingAIPrompt: Binding
- var isShowingAlert: Binding
-
- var toolbarStyle = ToolbarStyle.main
-
- init(isShowingCamera: Binding, isShowingFileSelection: Binding, isShowingPhotoLibrary: Binding,
- isShowingAIPrompt: Binding, isShowingAlert: Binding) {
- self.isShowingCamera = isShowingCamera
- self.isShowingFileSelection = isShowingFileSelection
- self.isShowingPhotoLibrary = isShowingPhotoLibrary
- self.isShowingAIPrompt = isShowingAIPrompt
- self.isShowingAlert = isShowingAlert
- super.init()
- }
-
- public func setBecomeFirstResponder() {
- webView.becomeFirstResponder()
- }
-
- lazy var contentBlocker = ContentBlocker(webView: editorWebView)
-
- private lazy var editorWebView: WKWebView = {
- let config = WKWebViewConfiguration()
- config.preferences = WKPreferences()
- config.preferences.minimumFontSize = 10
- config.preferences.javaScriptCanOpenWindowsAutomatically = false
- config.processPool = WKProcessPool()
- config.userContentController = WKUserContentController()
- config.setURLSchemeHandler(URLSchemeHandler(), forURLScheme: URLSchemeHandler.scheme)
-
- for jsMessageName in JSMessageName.allCases {
- config.userContentController.add(self, name: jsMessageName.rawValue)
- }
-
- let css = customCss ?? MessageWebViewUtils.loadAndFormatCSS(for: .editor)
- let cssStyle = "(() => { document.head.innerHTML += `\(css)`; })()"
- let cssScript = WKUserScript(source: cssStyle, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
- config.userContentController.addUserScript(cssScript)
-
- let _webView = WKWebView(frame: .zero, configuration: config)
- _webView.translatesAutoresizingMaskIntoConstraints = false
- _webView.navigationDelegate = self
- _webView.allowsLinkPreview = false
- _webView.addInputAccessoryView(toolbar: self.toolbar)
- _webView.isOpaque = false
- _webView.backgroundColor = .clear
- self.updateToolbarItems(style: .main)
- _webView.scrollView.keyboardDismissMode = .interactive
-
- #if DEBUG && !TEST
- if #available(iOS 17.0, *) {
- _webView.isInspectable = true
- }
- #endif
-
- return _webView
- }()
-
- override var webView: WKWebView {
- get {
- return editorWebView
- }
- set {
- editorWebView = newValue
- }
- }
-
- private func callEditorMethod(name: String, completion: ((_ error: Error?) -> Void)?) {
- webView.evaluateJavaScript("editor.\(name)()") { _, error in
- completion?(error)
- }
- }
-
- // MARK: - Custom function
-
- func insertRawHTML(_ html: String) async throws {
- let cleanedHTML = try await SwiftSoupUtils(fromHTMLFragment: html).cleanBody()
- try await webView.evaluateJavaScript("document.getElementById('editor').innerHTML = `\(cleanedHTML)`")
- }
-
- // MARK: - Editor methods
-
- /// Removes any current selection and moves the cursor to the very beginning of the document.
- func moveCursorToStart(completion: ((_ error: Error?) -> Void)? = nil) {
- callEditorMethod(name: "moveCursorToStart", completion: completion)
- }
-
- /// Removes any current selection and moves the cursor to the very end of the document.
- func moveCursorToEnd(completion: ((_ error: Error?) -> Void)? = nil) {
- callEditorMethod(name: "moveCursorToEnd", completion: completion)
- }
-
- func addBold(completion: ((_ error: Error?) -> Void)? = nil) {
- callEditorMethod(name: "bold", completion: completion)
- }
-
- func makeUnorderedList(completion: ((_ error: Error?) -> Void)? = nil) {
- callEditorMethod(name: "makeUnorderedList", completion: completion)
- }
-
- func removeList(completion: ((_ error: Error?) -> Void)? = nil) {
- callEditorMethod(name: "removeList", completion: completion)
- }
-
- // MARK: - Custom Toolbar
-
- public func updateToolbarItems(style: ToolbarStyle) {
- toolbarStyle = style
-
- let flexibleSpaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
-
- let actionItems = style.actions.map { action -> UIBarButtonItem in
- let item = UIBarButtonItem(
- image: action.icon,
- style: .plain,
- target: self,
- action: #selector(onToolbarClick(sender:))
- )
- item.tag = action.rawValue
- item.isSelected = action.isSelected(textAttribute: selectedTextAttribute)
- item.tintColor = action.tint
- if action == .editText && style == .textEdition {
- item.tintColor = UserDefaults.shared.accentColor.primary.color
- }
- return item
- }
- let barButtonItems = Array(actionItems.map { [$0] }.joined(separator: [flexibleSpaceItem]))
-
- toolbar.setItems(barButtonItems, animated: false)
- toolbar.setNeedsLayout()
- }
-
- public func getToolbar() -> UIToolbar {
- let newToolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 320, height: 48))
- UIConstants.applyComposeViewStyle(to: newToolbar)
- return newToolbar
- }
-
- @objc func onToolbarClick(sender: UIBarButtonItem) {
- guard let toolbarAction = ToolbarAction(rawValue: sender.tag) else { return }
-
- if let matomoName = toolbarAction.matomoName {
- matomo.track(eventWithCategory: .editorActions, name: matomoName)
- }
-
- switch toolbarAction {
- case .bold:
- bold()
- case .italic:
- italic()
- case .underline:
- underline()
- case .strikeThrough:
- strikethrough()
- case .unorderedList:
- makeUnorderedList()
- case .editText:
- updateToolbarItems(style: toolbarStyle == .main ? .textEdition : .main)
- case .ai:
- webView.resignFirstResponder()
- isShowingAIPrompt.wrappedValue = true
- case .addFile:
- isShowingFileSelection.wrappedValue = true
- case .addPhoto:
- isShowingPhotoLibrary.wrappedValue = true
- case .takePhoto:
- isShowingCamera.wrappedValue = true
- case .link:
- if selectedTextAttribute.format.hasLink {
- removeLink()
- } else {
- webView.resignFirstResponder()
- isShowingAlert.wrappedValue = NewMessageAlert(type: .link(handler: { url in
- self.makeLink(url: url)
- }))
- }
- case .programMessage:
- // TODO: Handle programmed message
- showWorkInProgressSnackBar()
- }
- }
-}
-
-enum ToolbarStyle {
- case main
- case textEdition
-
- var actions: [ToolbarAction] {
- switch self {
- case .main:
- @InjectService var featureFlagsManageable: FeatureFlagsManageable
- var mainActions: [ToolbarAction] = [.editText, .addFile, .addPhoto, .takePhoto, .link]
- featureFlagsManageable.feature(.aiMailComposer, on: {
- mainActions.insert(.ai, at: 1)
- }, off: nil)
- return mainActions
- case .textEdition:
- return [.editText, .bold, .italic, .underline, .strikeThrough, .unorderedList]
- }
- }
-}
-
-enum ToolbarAction: Int {
- case bold = 1
- case italic
- case underline
- case strikeThrough
- case unorderedList
- case editText
- case ai
- case addFile
- case addPhoto
- case takePhoto
- case link
- case programMessage
-
- var icon: UIImage {
- switch self {
- case .bold:
- return MailResourcesAsset.bold.image
- case .italic:
- return MailResourcesAsset.italic.image
- case .underline:
- return MailResourcesAsset.underline.image
- case .strikeThrough:
- return MailResourcesAsset.strikeThrough.image
- case .unorderedList:
- return MailResourcesAsset.unorderedList.image
- case .editText:
- return MailResourcesAsset.textModes.image
- case .ai:
- return MailResourcesAsset.aiWriter.image
- case .addFile:
- return MailResourcesAsset.folder.image
- case .addPhoto:
- return MailResourcesAsset.pictureLandscape.image
- case .takePhoto:
- return MailResourcesAsset.photo.image
- case .link:
- return MailResourcesAsset.hyperlink.image
- case .programMessage:
- return MailResourcesAsset.waitingMessage.image
- }
- }
-
- var tint: UIColor {
- if self == .ai {
- return MailResourcesAsset.aiColor.color
- } else {
- return MailResourcesAsset.textSecondaryColor.color
- }
- }
-
- var matomoName: String? {
- switch self {
- case .bold:
- return "bold"
- case .italic:
- return "italic"
- case .underline:
- return "underline"
- case .strikeThrough:
- return "strikeThrough"
- case .unorderedList:
- return "unorderedList"
- case .ai:
- return "aiWriter"
- case .addFile:
- return "importFile"
- case .addPhoto:
- return "importImage"
- case .takePhoto:
- return "importFromCamera"
- case .link:
- return "addLink"
- case .programMessage:
- return "postpone"
- default:
- return nil
- }
- }
-
- func isSelected(textAttributes: RETextAttributes) -> Bool {
- switch self {
- case .bold:
- return textAttributes.format.hasBold
- case .italic:
- return textAttributes.format.hasItalic
- case .underline:
- return textAttributes.format.hasUnderline
- case .strikeThrough:
- return textAttributes.format.hasStrikeThrough
- case .link:
- return false
- case .unorderedList, .editText, .ai, .addFile, .addPhoto, .takePhoto, .programMessage:
- return false
- }
- }
-
- func isSelected(textAttribute: SQTextAttribute) -> Bool {
- switch self {
- case .bold:
- return textAttribute.format.hasBold
- case .italic:
- return textAttribute.format.hasItalic
- case .underline:
- return textAttribute.format.hasUnderline
- case .strikeThrough:
- return textAttribute.format.hasStrikethrough
- case .link:
- return textAttribute.format.hasLink
- case .unorderedList, .editText, .ai, .addFile, .addPhoto, .takePhoto, .programMessage:
- return false
- }
- }
-}
diff --git a/Mail/Views/New Message/ComposeMessageBodyView.swift b/Mail/Views/New Message/ComposeMessageBodyView.swift
index 4d00248507..9970e3e33a 100644
--- a/Mail/Views/New Message/ComposeMessageBodyView.swift
+++ b/Mail/Views/New Message/ComposeMessageBodyView.swift
@@ -24,14 +24,13 @@ import SwiftModalPresentation
import SwiftUI
struct ComposeMessageBodyView: View {
- @State private var height = CGFloat.zero
@State private var isShowingCamera = false
@ModalState(context: ContextKeys.compose) private var isShowingFileSelection = false
@ModalState(context: ContextKeys.compose) private var isShowingPhotoLibrary = false
@ObservedRealmObject var draft: Draft
- @Binding var editorModel: RichTextEditorModel
+ @Binding var editorModel: EditorModel
@Binding var editorFocus: Bool
@Binding var currentSignature: Signature?
@Binding var isShowingAIPrompt: Bool
@@ -51,14 +50,14 @@ struct ComposeMessageBodyView: View {
EditorView(
body: $draft.body,
- height: $height,
+ model: $editorModel,
isShowingFileSelection: $isShowingFileSelection,
isShowingCamera: $isShowingCamera,
isShowingPhotoLibrary: $isShowingPhotoLibrary,
isShowingAIPrompt: $isShowingAIPrompt,
isShowingAlert: $isShowingAlert
)
- .frame(height: height)
+ .frame(height: editorModel.height)
}
.fullScreenCover(isPresented: $isShowingCamera) {
CameraPicker { data in
@@ -96,7 +95,7 @@ struct ComposeMessageBodyView: View {
#Preview {
let draft = Draft()
return ComposeMessageBodyView(draft: draft,
- editorModel: .constant(RichTextEditorModel()),
+ editorModel: .constant(EditorModel()),
editorFocus: .constant(false),
currentSignature: .constant(nil),
isShowingAIPrompt: .constant(false),
diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift
index 75bf515124..f95542aabd 100644
--- a/Mail/Views/New Message/ComposeMessageView.swift
+++ b/Mail/Views/New Message/ComposeMessageView.swift
@@ -84,7 +84,7 @@ struct ComposeMessageView: View {
@State private var currentSignature: Signature?
@State private var initialAttachments = [Attachable]()
- @State private var editorModel = RichTextEditorModel()
+ @State private var editorModel = EditorModel()
@Weak private var scrollView: UIScrollView?
@StateObject private var attachmentsManager: AttachmentsManager
@@ -192,16 +192,17 @@ struct ComposeMessageView: View {
self.scrollView = scrollView
scrollView.keyboardDismissMode = .interactive
}
- .onChange(of: editorModel.height) { _ in
- guard let scrollView else { return }
-
- let fullSize = scrollView.contentSize.height
- let realPosition = (fullSize - editorModel.height) + editorModel.cursorPosition
-
- guard realPosition >= 0 else { return }
- let rect = CGRect(x: 0, y: realPosition, width: 1, height: 1)
- scrollView.scrollRectToVisible(rect, animated: true)
- }
+// .onChange(of: editorModel.height) { _ in
+// guard let scrollView else { return }
+//
+// let fullSize = scrollView.contentSize.height
+// let realPosition = (fullSize - editorModel.height) + editorModel.cursorPosition
+//
+// guard realPosition >= 0 else { return }
+// let rect = CGRect(x: 0, y: realPosition, width: 1, height: 1)
+// scrollView.scrollRectToVisible(rect, animated: true)
+// }
+ .onChange(of: editorModel.cursorPosition, perform: keepCursorVisible)
.onChange(of: autocompletionType) { newValue in
guard newValue != nil else { return }
@@ -352,6 +353,12 @@ struct ComposeMessageView: View {
}
dismissMessageView()
}
+
+ private func keepCursorVisible(_ cursorPosition: CGPoint?) {
+ guard let scrollView, let cursorPosition else {
+ return
+ }
+ }
}
#Preview {
diff --git a/Mail/Views/New Message/MailRichEditor/EditorCoordinator.swift b/Mail/Views/New Message/MailRichEditor/EditorCoordinator.swift
index 5468ddcdf4..d750ef94f5 100644
--- a/Mail/Views/New Message/MailRichEditor/EditorCoordinator.swift
+++ b/Mail/Views/New Message/MailRichEditor/EditorCoordinator.swift
@@ -25,7 +25,7 @@ final class EditorCoordinator {
let toolbar: UIToolbar!
private(set) var parent: EditorView
- private(set) var toolbarStyle = ToolbarStyle.main
+ private(set) var toolbarStyle = EditorToolbarStyle.main
init(parent: EditorView) {
self.parent = parent
@@ -50,9 +50,12 @@ extension EditorCoordinator: RichEditorViewDelegate {
parent.body = richEditorView.html
}
+ func richEditorViewDidChangeSelection(_ richEditorView: RichEditorView) {
+ parent.model.cursorPosition = richEditorView.selection?.anchorPoint
+ }
+
func richEditorView(_ richEditorView: RichEditorView, contentHeightDidChange contentHeight: CGFloat) {
- parent.height = contentHeight
- print("NEW HEIGHT", contentHeight)
+ parent.model.height = contentHeight
}
func richEditorView(_ richEditorView: RichEditorView, selectedTextAttributesDidChange textAttributes: RETextAttributes) {
@@ -67,7 +70,7 @@ extension EditorCoordinator {
UIConstants.applyComposeViewStyle(to: toolbar)
}
- public func updateToolbarItems(for richEditorView: RichEditorView, style: ToolbarStyle) {
+ public func updateToolbarItems(for richEditorView: RichEditorView, style: EditorToolbarStyle) {
toolbarStyle = style
let flexibleSpaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil)
@@ -94,7 +97,7 @@ extension EditorCoordinator {
}
@objc private func onToolbarClick(_ sender: UIBarButtonItem, for richEditorView: RichEditorView) {
- guard let toolbarAction = ToolbarAction(rawValue: sender.tag) else { return }
+ guard let toolbarAction = EditorToolbarAction(rawValue: sender.tag) else { return }
switch toolbarAction {
case .ai, .addFile, .addPhoto, .takePhoto, .programMessage:
@@ -104,7 +107,7 @@ extension EditorCoordinator {
}
}
- private func performAppAction(_ action: ToolbarAction) {
+ private func performAppAction(_ action: EditorToolbarAction) {
switch action {
case .ai:
parent.isShowingAIPrompt = true
@@ -121,10 +124,10 @@ extension EditorCoordinator {
}
}
- private func performFormatAction(_ action: ToolbarAction, for richEditorView: RichEditorView) {
+ private func performFormatAction(_ action: EditorToolbarAction, for richEditorView: RichEditorView) {
switch action {
case .editText:
- let newToolbarStyle: ToolbarStyle = toolbarStyle == .main ? .textEdition : .main
+ let newToolbarStyle: EditorToolbarStyle = toolbarStyle == .main ? .textEdition : .main
updateToolbarItems(for: richEditorView, style: newToolbarStyle)
case .bold:
richEditorView.bold()
diff --git a/Mail/Views/New Message/MailRichEditor/EditorToolbar.swift b/Mail/Views/New Message/MailRichEditor/EditorToolbar.swift
new file mode 100644
index 0000000000..b0181cbe96
--- /dev/null
+++ b/Mail/Views/New Message/MailRichEditor/EditorToolbar.swift
@@ -0,0 +1,140 @@
+/*
+ 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 .
+ */
+
+import InfomaniakDI
+import InfomaniakRichEditor
+import MailCore
+import MailResources
+import UIKit
+
+enum EditorToolbarStyle {
+ case main
+ case textEdition
+
+ var actions: [EditorToolbarAction] {
+ switch self {
+ case .main:
+ @InjectService var featureFlagsManageable: FeatureFlagsManageable
+ var mainActions: [EditorToolbarAction] = [.editText, .addFile, .addPhoto, .takePhoto, .link]
+ featureFlagsManageable.feature(.aiMailComposer, on: {
+ mainActions.insert(.ai, at: 1)
+ }, off: nil)
+ return mainActions
+ case .textEdition:
+ return [.editText, .bold, .italic, .underline, .strikeThrough, .unorderedList]
+ }
+ }
+}
+
+enum EditorToolbarAction: Int {
+ case bold = 1
+ case italic
+ case underline
+ case strikeThrough
+ case unorderedList
+ case editText
+ case ai
+ case addFile
+ case addPhoto
+ case takePhoto
+ case link
+ case programMessage
+
+ var icon: UIImage {
+ switch self {
+ case .bold:
+ return MailResourcesAsset.bold.image
+ case .italic:
+ return MailResourcesAsset.italic.image
+ case .underline:
+ return MailResourcesAsset.underline.image
+ case .strikeThrough:
+ return MailResourcesAsset.strikeThrough.image
+ case .unorderedList:
+ return MailResourcesAsset.unorderedList.image
+ case .editText:
+ return MailResourcesAsset.textModes.image
+ case .ai:
+ return MailResourcesAsset.aiWriter.image
+ case .addFile:
+ return MailResourcesAsset.folder.image
+ case .addPhoto:
+ return MailResourcesAsset.pictureLandscape.image
+ case .takePhoto:
+ return MailResourcesAsset.photo.image
+ case .link:
+ return MailResourcesAsset.hyperlink.image
+ case .programMessage:
+ return MailResourcesAsset.waitingMessage.image
+ }
+ }
+
+ var tint: UIColor {
+ if self == .ai {
+ return MailResourcesAsset.aiColor.color
+ } else {
+ return MailResourcesAsset.textSecondaryColor.color
+ }
+ }
+
+ var matomoName: String? {
+ switch self {
+ case .bold:
+ return "bold"
+ case .italic:
+ return "italic"
+ case .underline:
+ return "underline"
+ case .strikeThrough:
+ return "strikeThrough"
+ case .unorderedList:
+ return "unorderedList"
+ case .ai:
+ return "aiWriter"
+ case .addFile:
+ return "importFile"
+ case .addPhoto:
+ return "importImage"
+ case .takePhoto:
+ return "importFromCamera"
+ case .link:
+ return "addLink"
+ case .programMessage:
+ return "postpone"
+ default:
+ return nil
+ }
+ }
+
+ func isSelected(textAttributes: RETextAttributes) -> Bool {
+ switch self {
+ case .bold:
+ return textAttributes.format.hasBold
+ case .italic:
+ return textAttributes.format.hasItalic
+ case .underline:
+ return textAttributes.format.hasUnderline
+ case .strikeThrough:
+ return textAttributes.format.hasStrikeThrough
+ case .link:
+ return false
+ case .unorderedList, .editText, .ai, .addFile, .addPhoto, .takePhoto, .programMessage:
+ return false
+ }
+ }
+}
diff --git a/Mail/Views/New Message/MailRichEditor/EditorView.swift b/Mail/Views/New Message/MailRichEditor/EditorView.swift
index 55594c2ffe..1348192721 100644
--- a/Mail/Views/New Message/MailRichEditor/EditorView.swift
+++ b/Mail/Views/New Message/MailRichEditor/EditorView.swift
@@ -20,9 +20,16 @@ import InfomaniakRichEditor
import MailCore
import SwiftUI
+struct EditorModel {
+ var height: CGFloat = .zero
+ var cursorPosition: CGPoint? = .zero
+}
+
struct EditorView: UIViewRepresentable {
@Binding var body: String
- @Binding var height: CGFloat
+
+ @Binding var model: EditorModel
+
@Binding var isShowingFileSelection: Bool
@Binding var isShowingCamera: Bool
@Binding var isShowingPhotoLibrary: Bool
@@ -50,7 +57,7 @@ struct EditorView: UIViewRepresentable {
#Preview {
EditorView(body: .constant(""),
- height: .constant(.zero),
+ model: .constant(EditorModel()),
isShowingFileSelection: .constant(false),
isShowingCamera: .constant(false),
isShowingPhotoLibrary: .constant(false),