>()
+ fun resetAiState() {
+ aiPrompt = ""
+ history.clear()
+ conversationContextId = null
+ aiPropositionStatusLiveData.value = null
+ }
+
fun generateNewAiProposition(
currentMailboxUuid: String,
formattedRecipientsString: String?,
diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/BodyContentPayload.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/BodyContentPayload.kt
index 1440718db39..b9805304ab4 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/BodyContentPayload.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/BodyContentPayload.kt
@@ -17,6 +17,8 @@
*/
package com.infomaniak.mail.ui.newMessage
+import com.infomaniak.mail.ui.newMessage.EditorContentManager.Companion.toSanitizedHtml
+
/**
* @param content The string representation of the body in either html or plain text format.
* @param type The type of representation of [content]. Each type will lead to different processing of the content.
@@ -26,6 +28,11 @@ data class BodyContentPayload(val content: String, val type: BodyContentType) {
companion object {
// Add some empty lines in the body, so the body focuses on these lines and not in the signature
fun emptyBody() = BodyContentPayload(content = "
", type = BodyContentType.HTML_SANITIZED)
+
+ fun bodyOf(body: BodyContentPayload) = BodyContentPayload(
+ content = "${body.toSanitizedHtml()}
",
+ type = BodyContentType.HTML_SANITIZED
+ )
}
}
diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt
index feffa90df38..5821216f4cd 100644
--- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt
+++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt
@@ -54,6 +54,7 @@ import com.infomaniak.mail.MatomoMail.trackSendingDraftEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.data.LocalSettings
import com.infomaniak.mail.data.api.ApiRepository
+import com.infomaniak.mail.data.cache.AiDraftCache
import com.infomaniak.mail.data.cache.RealmDatabase
import com.infomaniak.mail.data.cache.mailboxContent.DraftController
import com.infomaniak.mail.data.cache.mailboxContent.MessageController
@@ -97,10 +98,10 @@ import com.infomaniak.mail.utils.DraftInitManager
import com.infomaniak.mail.utils.HtmlFormatter.Companion.MENTIONS_STYLE
import com.infomaniak.mail.utils.JsoupParserUtil.jsoupParseWithLog
import com.infomaniak.mail.utils.LocalStorageUtils
-import com.infomaniak.mail.utils.MessageBodyUtils
import com.infomaniak.mail.utils.MessageBodyUtils.EDITOR_LOCAL_SIGNATURE_ID
import com.infomaniak.mail.utils.MessageBodyUtils.INFOMANIAK_FORWARD_QUOTE_HTML_CLASS_NAME
import com.infomaniak.mail.utils.MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME
+import com.infomaniak.mail.utils.MessageBodyUtils.asPlainText
import com.infomaniak.mail.utils.MessageBodyUtils.isHtmlBlank
import com.infomaniak.mail.utils.MessageBodyUtils.splitSignatureAndQuoteFromBody
import com.infomaniak.mail.utils.SentryDebug
@@ -109,7 +110,6 @@ import com.infomaniak.mail.utils.Utils
import com.infomaniak.mail.utils.coroutineContext
import com.infomaniak.mail.utils.extensions.AttachmentExt.findSpecificAttachment
import com.infomaniak.mail.utils.extensions.appContext
-import com.infomaniak.mail.utils.extensions.htmlToText
import com.infomaniak.mail.utils.extensions.valueOrEmpty
import com.infomaniak.mail.utils.uploadAttachmentsWithMutex
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -158,6 +158,7 @@ class NewMessageViewModel @Inject constructor(
application: Application,
private val savedStateHandle: SavedStateHandle,
private val aiSharedData: AiSharedData,
+ private val aiDraftCache: AiDraftCache,
private val draftController: DraftController,
private val globalCoroutineScope: CoroutineScope,
private val mailboxContentRealm: RealmDatabase.MailboxContent,
@@ -368,6 +369,17 @@ class NewMessageViewModel @Inject constructor(
dismissNotification()
markAsRead(currentMailbox(), realm)
+ if (isNewMessage) {
+ aiDraftCache.pendingAiContent?.let { pendingAiContent ->
+ aiDraftCache.pendingAiSubject?.let { subject -> draft.subject = subject }
+ initialBody = BodyContentPayload.bodyOf(
+ BodyContentPayload(pendingAiContent, BodyContentType.TEXT_PLAIN_WITHOUT_HTML)
+ )
+ aiDraftCache.pendingAiSubject = null
+ aiDraftCache.pendingAiContent = null
+ }
+ }
+
realm.write { DraftController.upsertDraftBlocking(it, realm = this) }
it.initLiveData(signatures)
@@ -673,18 +685,6 @@ class NewMessageViewModel @Inject constructor(
}
}
- private suspend fun Body.asPlainText(): String? {
- // TODO: When the API handles blank characters, remove ifBlank
- return when (type) {
- Utils.TEXT_HTML -> {
- val splitBodyContent = MessageBodyUtils.splitContentAndQuote(this).content
- val fullBody = MessageBodyUtils.mergeSplitBodyAndSubBodies(splitBodyContent, subBodies)
- fullBody.htmlToText()
- }
- else -> value
- }.ifBlank { null }
- }
-
private suspend fun handleSingleSendIntent(draft: Draft, intent: Intent) = with(intent) {
if (hasExtra(Intent.EXTRA_EMAIL)) {
diff --git a/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt
index d0e8d0d7ae6..16167521c14 100644
--- a/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt
+++ b/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt
@@ -107,6 +107,18 @@ object MessageBodyUtils {
)
}
+ suspend fun Body.asPlainText(): String? {
+ // TODO: When the API handles blank characters, remove ifBlank
+ return when (type) {
+ Utils.TEXT_HTML -> {
+ val splitBodyContent = splitContentAndQuote(this).content
+ val fullBody = mergeSplitBodyAndSubBodies(splitBodyContent, subBodies)
+ fullBody.htmlToText()
+ }
+ else -> value
+ }.ifBlank { null }
+ }
+
private fun CoroutineScope.splitContentAndQuotes(htmlDocument: Document): Pair> {
val quotes = mutableListOf()
diff --git a/app/src/main/java/com/infomaniak/mail/utils/date/DateFormatUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/date/DateFormatUtils.kt
index 43b9380eb4d..9209081d6fe 100644
--- a/app/src/main/java/com/infomaniak/mail/utils/date/DateFormatUtils.kt
+++ b/app/src/main/java/com/infomaniak/mail/utils/date/DateFormatUtils.kt
@@ -32,9 +32,12 @@ object DateFormatUtils {
private const val FORMAT_DATE_12_HOUR = "hh:mm a"
private const val FORMAT_DATE_WITH_YEAR = "d MMM yyyy"
private const val FORMAT_DATE_WITHOUT_YEAR = "d MMM"
+ private const val FORMAT_DATE_SHORT_NUMERIC = "dd/MM/yy"
fun Context.formatTime(date: Date): String = date.format(localHourFormat())
+ fun shortDateWithoutYear(date: Date): String = date.format(FORMAT_DATE_WITHOUT_YEAR)
+
fun Context.fullDateWithoutYear(date: Date): String = formatDateTime(date, FORMAT_DATE_WITHOUT_YEAR, localHourFormat())
fun Context.fullDateWithYear(date: Date): String = formatDateTime(date, FORMAT_DATE_WITH_YEAR, localHourFormat())
@@ -43,6 +46,8 @@ object DateFormatUtils {
fun Context.dayOfWeekDateWithYear(date: Date): String = formatDateTime(date, FORMAT_DATE_DAY_MONTH_YEAR, localHourFormat())
+ fun shortNumericDate(date: Date): String = date.format(FORMAT_DATE_SHORT_NUMERIC)
+
private fun Context.formatDateTime(date: Date, dateFormat: String, timeFormat: String) = getString(
R.string.messageDetailsDateAt,
date.format(dateFormat),
diff --git a/app/src/main/java/com/infomaniak/mail/utils/date/MailDateFormatUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/date/MailDateFormatUtils.kt
index 791af096e1e..cfef65cb2cc 100644
--- a/app/src/main/java/com/infomaniak/mail/utils/date/MailDateFormatUtils.kt
+++ b/app/src/main/java/com/infomaniak/mail/utils/date/MailDateFormatUtils.kt
@@ -27,8 +27,8 @@ import com.infomaniak.mail.R
import com.infomaniak.mail.utils.date.DateFormatUtils.dayOfWeekDateWithYear
import com.infomaniak.mail.utils.date.DateFormatUtils.dayOfWeekDateWithoutYear
import com.infomaniak.mail.utils.date.DateFormatUtils.formatTime
-import com.infomaniak.mail.utils.date.DateFormatUtils.fullDateWithYear
-import com.infomaniak.mail.utils.date.DateFormatUtils.fullDateWithoutYear
+import com.infomaniak.mail.utils.date.DateFormatUtils.shortDateWithoutYear
+import com.infomaniak.mail.utils.date.DateFormatUtils.shortNumericDate
import java.time.format.FormatStyle
import java.util.Date
@@ -37,13 +37,9 @@ object MailDateFormatUtils {
fun Context.mailFormattedDate(date: Date): CharSequence = with(date) {
return when {
isToday() -> formatTime(this)
- isYesterday() -> getString(
- R.string.messageDetailsDateAt,
- getString(R.string.messageDetailsYesterday),
- formatTime(this),
- )
- isThisYear() -> fullDateWithoutYear(this)
- else -> fullDateWithYear(this)
+ isYesterday() -> getString(R.string.messageDetailsYesterday)
+ isThisYear() -> shortDateWithoutYear(this)
+ else -> shortNumericDate(this)
}
}
diff --git a/app/src/main/java/com/infomaniak/mail/views/AiPromptView.kt b/app/src/main/java/com/infomaniak/mail/views/AiPromptView.kt
new file mode 100644
index 00000000000..be99a60c7f4
--- /dev/null
+++ b/app/src/main/java/com/infomaniak/mail/views/AiPromptView.kt
@@ -0,0 +1,63 @@
+/*
+ * Infomaniak Mail - Android
+ * Copyright (C) 2026 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 .
+ */
+
+package com.infomaniak.mail.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.FrameLayout
+import androidx.core.widget.doAfterTextChanged
+import com.infomaniak.core.legacy.utils.showKeyboard
+import com.infomaniak.mail.databinding.ViewAiPromptContentBinding
+
+class AiPromptView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : FrameLayout(context, attrs, defStyleAttr) {
+
+ private val binding = ViewAiPromptContentBinding.inflate(LayoutInflater.from(context), this, true)
+ private var onPromptChanged: ((String) -> Unit)? = null
+
+ fun bind(
+ prompt: CharSequence?,
+ placeholder: CharSequence,
+ onCloseClick: () -> Unit,
+ onGenerateClick: () -> Unit,
+ onPromptChanged: (String) -> Unit,
+ ) = with(binding) {
+ this@AiPromptView.onPromptChanged = onPromptChanged
+
+ this.prompt.setText(prompt)
+ this.prompt.setSelection(this.prompt.text?.length ?: 0)
+ this.prompt.hint = placeholder
+
+ closeButton.setOnClickListener { onCloseClick() }
+ generateButton.setOnClickListener { onGenerateClick() }
+
+ this.prompt.doAfterTextChanged { text ->
+ generateButton.isEnabled = text?.isNotEmpty() == true
+ this@AiPromptView.onPromptChanged?.invoke(text?.toString().orEmpty())
+ }
+
+ generateButton.isEnabled = this.prompt.text?.isNotEmpty() == true
+ }
+
+ fun focusPrompt() = binding.prompt.showKeyboard()
+}
diff --git a/app/src/main/res/drawable/bg_new_feature_badge.xml b/app/src/main/res/drawable/bg_new_feature_badge.xml
new file mode 100644
index 00000000000..5323ed953a4
--- /dev/null
+++ b/app/src/main/res/drawable/bg_new_feature_badge.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_reply_euria.xml b/app/src/main/res/drawable/ic_reply_euria.xml
new file mode 100644
index 00000000000..b6ad0e11f0f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_reply_euria.xml
@@ -0,0 +1,27 @@
+
+
+
+
diff --git a/app/src/main/res/layout/bottom_sheet_ask_euria_actions.xml b/app/src/main/res/layout/bottom_sheet_ask_euria_actions.xml
index 950f5cc3b6e..3c5286af734 100644
--- a/app/src/main/res/layout/bottom_sheet_ask_euria_actions.xml
+++ b/app/src/main/res/layout/bottom_sheet_ask_euria_actions.xml
@@ -30,6 +30,13 @@
app:title="@string/actionSummarize"
app:visibleDivider="false" />
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_message.xml b/app/src/main/res/layout/item_message.xml
index fa43bcbd84b..8bb9e61fdfb 100644
--- a/app/src/main/res/layout/item_message.xml
+++ b/app/src/main/res/layout/item_message.xml
@@ -175,6 +175,14 @@
app:iconTint="@color/redDestructiveAction"
tools:visibility="visible" />
+
+
+
diff --git a/app/src/main/res/layout/fragment_ai_prompt.xml b/app/src/main/res/layout/view_ai_prompt_content.xml
similarity index 99%
rename from app/src/main/res/layout/fragment_ai_prompt.xml
rename to app/src/main/res/layout/view_ai_prompt_content.xml
index b3f2911c872..8d06e0d66b7 100644
--- a/app/src/main/res/layout/fragment_ai_prompt.xml
+++ b/app/src/main/res/layout/view_ai_prompt_content.xml
@@ -1,6 +1,6 @@