Skip to content

Commit

Permalink
Merge pull request #1010 from Infomaniak/ai
Browse files Browse the repository at this point in the history
feat: AI Writer
  • Loading branch information
PhilippeWeidmann committed Oct 3, 2023
2 parents 9a24c9d + 4e64f83 commit 09d51bb
Show file tree
Hide file tree
Showing 66 changed files with 1,717 additions and 190 deletions.
26 changes: 26 additions & 0 deletions Mail/Components/Buttons/MailButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ struct MailButtonStyleKey: EnvironmentKey {
static var defaultValue = MailButton.Style.large
}

struct MailButtonPrimaryColorKey: EnvironmentKey {
static var defaultValue = Color.accentColor
}

struct MailButtonSecondaryColorKey: EnvironmentKey {
static var defaultValue = UserDefaults.shared.accentColor.onAccent.swiftUIColor
}

struct MailButtonFullWidthKey: EnvironmentKey {
static var defaultValue = false
}
Expand All @@ -48,6 +56,16 @@ extension EnvironmentValues {
set { self[MailButtonStyleKey.self] = newValue }
}

var mailButtonPrimaryColor: Color {
get { self[MailButtonPrimaryColorKey.self] }
set { self[MailButtonPrimaryColorKey.self] = newValue }
}

var mailButtonSecondaryColor: Color {
get { self[MailButtonSecondaryColorKey.self] }
set { self[MailButtonSecondaryColorKey.self] = newValue }
}

var mailButtonFullWidth: Bool {
get { self[MailButtonFullWidthKey.self] }
set { self[MailButtonFullWidthKey.self] = newValue }
Expand All @@ -74,6 +92,14 @@ extension View {
environment(\.mailButtonStyle, style)
}

func mailButtonPrimaryColor(_ color: Color) -> some View {
environment(\.mailButtonPrimaryColor, color)
}

func mailButtonSecondaryColor(_ color: Color) -> some View {
environment(\.mailButtonSecondaryColor, color)
}

func mailButtonFullWidth(_ fullWidth: Bool) -> some View {
environment(\.mailButtonFullWidth, fullWidth)
}
Expand Down
60 changes: 43 additions & 17 deletions Mail/Components/Buttons/MailButtonStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ struct MailButtonStyle: ButtonStyle {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.isEnabled) private var isEnabled

@Environment(\.mailButtonPrimaryColor) private var mailButtonPrimaryColor: Color
@Environment(\.mailButtonSecondaryColor) private var mailButtonSecondaryColor: Color
@Environment(\.mailButtonFullWidth) private var fullWidth: Bool
@Environment(\.mailButtonMinimizeHeight) private var minimizeHeight: Bool

Expand All @@ -34,11 +36,11 @@ struct MailButtonStyle: ButtonStyle {
guard !minimizeHeight else { return nil }

if style == .floatingActionButton {
return iconOnlyButton ? 64 : 56
return iconOnlyButton ? UIConstants.buttonLargeHeight : UIConstants.buttonMediumHeight
} else if fullWidth {
return 56
return UIConstants.buttonMediumHeight
} else {
return 40
return UIConstants.buttonSmallHeight
}
}

Expand All @@ -57,49 +59,73 @@ struct MailButtonStyle: ButtonStyle {
linkStyle(configuration: configuration)
}
}
}

// MARK: - Large style helpers

extension MailButtonStyle {
@ViewBuilder private func largeStyle(configuration: Configuration) -> some View {
configuration.label
.textStyle(isEnabled ? .bodyMediumOnAccent : .bodyMediumOnDisabled)
.foregroundColor(largeTextColor())
.textStyle(.bodyMedium)
.padding(.horizontal, value: .medium)
.frame(width: buttonWidth, height: buttonHeight)
.background(largeBackground(configuration: configuration))
.clipShape(RoundedRectangle(cornerRadius: UIConstants.buttonsRadius))
.brightness(largeBrightness(configuration: configuration))
}

@ViewBuilder private func linkStyle(configuration: Configuration) -> some View {
configuration.label
.textStyle(linkTextStyle())
.opacity(configuration.isPressed ? 0.7 : 1)
.frame(width: buttonWidth, height: buttonHeight)
}

private func largeBackground(configuration: Configuration) -> Color {
guard isEnabled else { return MailResourcesAsset.textTertiaryColor.swiftUIColor }

var opacity = 1.0
if colorScheme == .light {
opacity = configuration.isPressed ? 0.8 : 1
}
return .accentColor.opacity(opacity)

return mailButtonPrimaryColor.opacity(opacity)
}

private func largeTextColor() -> Color {
guard isEnabled else { return MailTextStyle.bodyMediumOnDisabled.color }
return mailButtonSecondaryColor
}

private func largeBrightness(configuration: Configuration) -> Double {
guard colorScheme == .dark else { return 0 }
return configuration.isPressed ? 0.1 : 0
}
}

// MARK: - Link style helpers

extension MailButtonStyle {
@ViewBuilder private func linkStyle(configuration: Configuration) -> some View {
configuration.label
.foregroundColor(linkTextColor())
.textStyle(linkTextStyle())
.opacity(configuration.isPressed ? 0.7 : 1)
.frame(width: buttonWidth, height: buttonHeight)
}

private func linkTextStyle() -> MailTextStyle {
switch style {
case .link:
return .bodyMediumAccent
case .link, .destructive:
return .bodyMedium
case .smallLink:
return .bodySmallAccent
case .destructive:
return .bodyMediumError
return .bodySmall
default:
return .body
}
}

private func linkTextColor() -> Color {
guard isEnabled else { return MailTextStyle.bodyMediumOnDisabled.color }

if style == .destructive {
return MailTextStyle.bodyMediumError.color
} else {
return mailButtonPrimaryColor
}
}
}
2 changes: 1 addition & 1 deletion Mail/Components/RecipientChipLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class RecipientChipLabel: UILabel, UIKeyInput {

public func updateColors(isFirstResponder: Bool) {
if isExternal {
textColor = isFirstResponder ? MailResourcesAsset.onTagColor.color : MailResourcesAsset.textPrimaryColor.color
textColor = isFirstResponder ? MailResourcesAsset.onTagExternalColor.color : MailResourcesAsset.textPrimaryColor.color
borderColor = MailResourcesAsset.yellowColor.color
backgroundColor = isFirstResponder ? MailResourcesAsset.yellowColor.color : MailResourcesAsset.textFieldColor.color
} else {
Expand Down
3 changes: 3 additions & 0 deletions Mail/Helpers/AppAssembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ enum ApplicationAssembly {
Factory(type: InfomaniakNotifications.self) { _, _ in
InfomaniakNotifications(appGroup: AccountManager.appGroup)
},
Factory(type: FeatureFlagsManageable.self) { _, _ in
FeatureFlagsManager()
},
Factory(type: AppLockHelper.self) { _, _ in
AppLockHelper()
},
Expand Down
80 changes: 51 additions & 29 deletions Mail/Helpers/RichTextEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,15 @@ import SwiftUI
import WebKit

struct RichTextEditor: UIViewRepresentable {
typealias UIViewType = MailEditorView

@State private var editorCurrentSignature: Signature?
@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 currentSignature: Signature?
@Binding var isShowingAIPrompt: Bool

let blockRemoteContent: Bool
var alert: ObservedObject<NewMessageAlert>.Wrapper
Expand All @@ -45,7 +43,7 @@ struct RichTextEditor: UIViewRepresentable {
alert: ObservedObject<NewMessageAlert>.Wrapper,
isShowingCamera: Binding<Bool>, isShowingFileSelection: Binding<Bool>, isShowingPhotoLibrary: Binding<Bool>,
becomeFirstResponder: Binding<Bool>,
currentSignature: Binding<Signature?>,
isShowingAIPrompt: Binding<Bool>,
blockRemoteContent: Bool) {
_model = model
_body = body
Expand All @@ -54,16 +52,21 @@ struct RichTextEditor: UIViewRepresentable {
_isShowingFileSelection = isShowingFileSelection
_isShowingPhotoLibrary = isShowingPhotoLibrary
_becomeFirstResponder = becomeFirstResponder
_currentSignature = currentSignature
_isShowingAIPrompt = isShowingAIPrompt
self.blockRemoteContent = blockRemoteContent
_editorCurrentSignature = State(wrappedValue: currentSignature.wrappedValue)
}

class Coordinator: SQTextEditorDelegate {
var parent: RichTextEditor

init(_ parent: RichTextEditor) {
self.parent = parent // tell the coordinator what its parent is, so it can modify values there directly
self.parent = parent
NotificationCenter.default.addObserver(
self,
selector: #selector(requireBodyUpdate),
name: Notification.Name.updateComposeMessageBody,
object: nil
)
}

@MainActor
Expand Down Expand Up @@ -116,6 +119,10 @@ struct RichTextEditor: UIViewRepresentable {
parent.body = content
}
}

@objc func requireBodyUpdate() {
parent.mustUpdateBody = true
}
}

func makeCoordinator() -> Coordinator {
Expand All @@ -126,7 +133,8 @@ struct RichTextEditor: UIViewRepresentable {
let richTextEditor = MailEditorView(alert: alert,
isShowingCamera: $isShowingCamera,
isShowingFileSelection: $isShowingFileSelection,
isShowingPhotoLibrary: $isShowingPhotoLibrary)
isShowingPhotoLibrary: $isShowingPhotoLibrary,
isShowingAIPrompt: $isShowingAIPrompt)
richTextEditor.delegate = context.coordinator
return richTextEditor
}
Expand All @@ -138,9 +146,9 @@ struct RichTextEditor: UIViewRepresentable {
becomeFirstResponder = false
}
}
if currentSignature != editorCurrentSignature {
if mustUpdateBody {
Task {
editorCurrentSignature = currentSignature
mustUpdateBody = false
try await context.coordinator.insertBody(editor: uiView)
}
}
Expand Down Expand Up @@ -176,15 +184,18 @@ class MailEditorView: SQTextEditorView {
var isShowingCamera: Binding<Bool>
var isShowingFileSelection: Binding<Bool>
var isShowingPhotoLibrary: Binding<Bool>
var isShowingAIPrompt: Binding<Bool>

var toolbarStyle = ToolbarStyle.main

init(alert: ObservedObject<NewMessageAlert>.Wrapper,
isShowingCamera: Binding<Bool>, isShowingFileSelection: Binding<Bool>, isShowingPhotoLibrary: Binding<Bool>) {
isShowingCamera: Binding<Bool>, isShowingFileSelection: Binding<Bool>, isShowingPhotoLibrary: Binding<Bool>,
isShowingAIPrompt: Binding<Bool>) {
self.alert = alert
self.isShowingCamera = isShowingCamera
self.isShowingFileSelection = isShowingFileSelection
self.isShowingPhotoLibrary = isShowingPhotoLibrary
self.isShowingAIPrompt = isShowingAIPrompt
super.init()
}

Expand Down Expand Up @@ -277,6 +288,7 @@ class MailEditorView: SQTextEditorView {
)
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
}
Expand All @@ -290,18 +302,7 @@ class MailEditorView: SQTextEditorView {

public func getToolbar() -> UIToolbar {
let newToolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 320, height: 48))
newToolbar.tintColor = MailResourcesAsset.textSecondaryColor.color
newToolbar.barTintColor = MailResourcesAsset.backgroundSecondaryColor.color
newToolbar.isTranslucent = false

// Shadow
newToolbar.setShadowImage(UIImage(), forToolbarPosition: .any)
newToolbar.layer.shadowColor = UIColor.black.cgColor
newToolbar.layer.shadowOpacity = 0.1
newToolbar.layer.shadowOffset = CGSize(width: 1, height: 1)
newToolbar.layer.shadowRadius = 2
newToolbar.layer.masksToBounds = false

UIConstants.applyComposeViewStyle(to: newToolbar)
return newToolbar
}

Expand All @@ -326,12 +327,15 @@ class MailEditorView: SQTextEditorView {
makeUnorderedList()
case .editText:
updateToolbarItems(style: toolbarStyle == .main ? .textEdition : .main)
case .ai:
webView.resignFirstResponder()
isShowingAIPrompt.wrappedValue = true
case .addFile:
isShowingFileSelection.wrappedValue.toggle()
isShowingFileSelection.wrappedValue = true
case .addPhoto:
isShowingPhotoLibrary.wrappedValue.toggle()
isShowingPhotoLibrary.wrappedValue = true
case .takePhoto:
isShowingCamera.wrappedValue.toggle()
isShowingCamera.wrappedValue = true
case .link:
if selectedTextAttribute.format.hasLink {
removeLink()
Expand All @@ -355,7 +359,12 @@ enum ToolbarStyle {
var actions: [ToolbarAction] {
switch self {
case .main:
return [.editText, .addFile, .addPhoto, .takePhoto, .link]
@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]
}
Expand All @@ -369,6 +378,7 @@ enum ToolbarAction: Int {
case strikeThrough
case unorderedList
case editText
case ai
case addFile
case addPhoto
case takePhoto
Expand All @@ -389,6 +399,8 @@ enum ToolbarAction: Int {
return MailResourcesAsset.unorderedList.image
case .editText:
return MailResourcesAsset.textModes.image
case .ai:
return MailResourcesAsset.aiWriter.image
case .addFile:
return MailResourcesAsset.folder.image
case .addPhoto:
Expand All @@ -402,6 +414,14 @@ enum ToolbarAction: Int {
}
}

var tint: UIColor {
if self == .ai {
return MailResourcesAsset.aiColor.color
} else {
return MailResourcesAsset.textSecondaryColor.color
}
}

var matomoName: String? {
switch self {
case .bold:
Expand All @@ -414,6 +434,8 @@ enum ToolbarAction: Int {
return "strikeThrough"
case .unorderedList:
return "unorderedList"
case .ai:
return "aiWriter"
case .addFile:
return "importFile"
case .addPhoto:
Expand Down Expand Up @@ -441,7 +463,7 @@ enum ToolbarAction: Int {
return textAttribute.format.hasStrikethrough
case .link:
return textAttribute.format.hasLink
case .unorderedList, .editText, .addFile, .addPhoto, .takePhoto, .programMessage:
case .unorderedList, .editText, .ai, .addFile, .addPhoto, .takePhoto, .programMessage:
return false
}
}
Expand Down
Loading

0 comments on commit 09d51bb

Please sign in to comment.