Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7619b73
feat: Add signature to rich editor
solrubado Feb 3, 2026
fcd192b
feat: Add signature and quotes to richHtlmEditorWebView
solrubado Feb 11, 2026
a46bb14
feat: Add everything into the mainwebview
solrubado Feb 12, 2026
cd7e225
feat: Add everything in the same webview and add button to hide quotes
solrubado Feb 12, 2026
fe08790
feat: Changes in quote toggle button and in webview focus
solrubado Feb 13, 2026
3ca6aab
refactor: Remove logs, unnused icon and use jsoupwithlogs
solrubado Feb 16, 2026
edce56d
fix: Add signature before quotes if they exist
solrubado Feb 16, 2026
dfa902d
feat: Show images on forward
solrubado Feb 16, 2026
97a30b6
fix: Use images cids and remove loadWithOverviewMode on new message
solrubado Feb 16, 2026
231e82e
refactor: Remove unnecesary log
solrubado Feb 16, 2026
abd5022
refactor: Remove unused function
solrubado Feb 16, 2026
7af67ba
refactor: Fix identation in style css
solrubado Feb 16, 2026
dbc22ef
refactor: Fix sonar issues
solrubado Feb 16, 2026
c70262f
feat: Add textview placeholder and change signature with js
solrubado Feb 25, 2026
a372005
feat: Remove splitting of mail body if it's a draft and fix save snap…
solrubado Feb 25, 2026
74605b9
fix: Fix cid images and webclient for attachments
solrubado Feb 26, 2026
06019ec
feat: Change 3 dots for text button and fix ai PR suggestions
solrubado Mar 2, 2026
2d6d51d
fix: Fix sonar issues and add more margin to signature
solrubado Mar 2, 2026
4694ee5
refactor: Refactor for PR
solrubado Mar 3, 2026
b9d10ac
fix: Remove unnecesary space
solrubado Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ package com.infomaniak.mail.ui.newMessage
data class BodyContentPayload(val content: String, val type: BodyContentType) {

companion object {
fun emptyBody() = BodyContentPayload(content = "", type = BodyContentType.TEXT_PLAIN_WITHOUT_HTML)
fun emptyBody() =
BodyContentPayload(
content = "<br id=lineBreakAtBeginningOfSignature>",
type = BodyContentType.HTML_SANITIZED
)
}
}

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import com.infomaniak.mail.ui.newMessage.NewMessageRecipientFieldsManager.FieldT
import com.infomaniak.mail.utils.AccountUtils
import com.infomaniak.mail.utils.ContactUtils.arrangeMergedContacts
import com.infomaniak.mail.utils.DraftInitManager
import com.infomaniak.mail.utils.JsoupParserUtil
import com.infomaniak.mail.utils.JsoupParserUtil.jsoupParseWithLog
import com.infomaniak.mail.utils.LocalStorageUtils
import com.infomaniak.mail.utils.MessageBodyUtils
Expand Down Expand Up @@ -130,7 +131,6 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.invoke
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jsoup.nodes.Document
import splitties.experimental.ExperimentalSplittiesApi
import java.util.Date
import javax.inject.Inject
Expand Down Expand Up @@ -171,8 +171,6 @@ class NewMessageViewModel @Inject constructor(
val ccLiveData = MutableLiveData<UiRecipients>()
val bccLiveData = MutableLiveData<UiRecipients>()
val attachmentsLiveData = MutableLiveData<List<Attachment>>()
val uiSignatureLiveData = MutableLiveData<String?>()
val uiQuoteLiveData = MutableLiveData<String?>()
inline val allRecipients get() = toLiveData.valueOrEmpty() + ccLiveData.valueOrEmpty() + bccLiveData.valueOrEmpty()
//endregion

Expand Down Expand Up @@ -222,6 +220,9 @@ class NewMessageViewModel @Inject constructor(
private val _isShimmering = MutableStateFlow(true)
val isShimmering: StateFlow<Boolean> = _isShimmering

private val _areQuotesVisible = MutableStateFlow(false)
val areQuotesVisible: StateFlow<Boolean> = _areQuotesVisible

//region Check mailbox existence
private val exitSignal: CompletableJob = Job()

Expand Down Expand Up @@ -328,10 +329,8 @@ class NewMessageViewModel @Inject constructor(
markAsRead(currentMailbox(), realm)

realm.write { DraftController.upsertDraftBlocking(it, realm = this) }
it.saveSnapshot(initialBody.content)
it.initLiveData(signatures)
_isShimmering.emit(false)

initResult.postValue(InitResult(it, signatures))
}

Expand All @@ -344,11 +343,14 @@ class NewMessageViewModel @Inject constructor(
if (draft.identityId.isNullOrBlank()) {
draft.identityId = currentMailbox().getDefaultSignatureWithFallback().id.toString()
}
splitSignatureAndQuoteFromBody(draft)
val contentType =
if (draft.mimeType == Utils.TEXT_PLAIN) BodyContentType.TEXT_PLAIN_WITH_HTML else BodyContentType.HTML_SANITIZED
initialBody = BodyContentPayload(draft.body, contentType)
return draft
}
}

private suspend fun getNewDraft(signatures: List<Signature>, intent: Intent, realm: Realm): Draft? = Draft().apply {
private suspend fun getNewDraft(signatures: List<Signature>, intent: Intent, realm: Realm): Draft = Draft().apply {

var previousMessage: Message? = null

Expand Down Expand Up @@ -424,56 +426,6 @@ class NewMessageViewModel @Inject constructor(
savedStateHandle[NewMessageActivityArgs::draftResource.name] = draftResource
}

private fun splitSignatureAndQuoteFromBody(draft: Draft) {
val remoteBody = draft.body
if (remoteBody.isEmpty()) return

val (body, signature, quote) = when (draft.mimeType) {
Utils.TEXT_PLAIN -> BodyData(
body = BodyContentPayload(remoteBody, BodyContentType.TEXT_PLAIN_WITHOUT_HTML),
signature = null,
quote = null
)
Utils.TEXT_HTML -> splitSignatureAndQuoteFromHtml(remoteBody)
else -> error("Cannot load an email which is not of type text/plain or text/html")
}

initialBody = body
initialSignature = signature
initialQuote = quote
}

private fun splitSignatureAndQuoteFromHtml(draftBody: String): BodyData {

fun Document.split(divClassName: String, defaultValue: String): Pair<String, String?> {
return getElementsByClass(divClassName).firstOrNull()?.let {
it.remove()
val first = body().html()
val second = if (it.html().isBlank()) null else it.outerHtml()
first to second
} ?: (defaultValue to null)
}

fun String.lastIndexOfOrMax(string: String): Int {
val index = lastIndexOf(string)
return if (index == -1) Int.MAX_VALUE else index
}

val doc = jsoupParseWithLog(draftBody).also { it.outputSettings().prettyPrint(false) }

val (bodyWithQuote, signature) = doc.split(MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_CLASS_NAME, draftBody)

val replyPosition = draftBody.lastIndexOfOrMax(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME)
val forwardPosition = draftBody.lastIndexOfOrMax(MessageBodyUtils.INFOMANIAK_FORWARD_QUOTE_HTML_CLASS_NAME)
val (body, quote) = if (replyPosition < forwardPosition) {
doc.split(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME, bodyWithQuote)
} else {
doc.split(MessageBodyUtils.INFOMANIAK_FORWARD_QUOTE_HTML_CLASS_NAME, bodyWithQuote)
}

return BodyData(BodyContentPayload(body, BodyContentType.HTML_UNSANITIZED), signature, quote)
}

private fun populateWithExternalMailDataIfNeeded(draft: Draft, intent: Intent) {
when (intent.action) {
Intent.ACTION_SEND -> handleSingleSendIntent(draft, intent)
Expand Down Expand Up @@ -548,10 +500,28 @@ class NewMessageViewModel @Inject constructor(

attachmentsLiveData.postValue(attachments)

editorBodyInitializer.postValue(initialBody)
var finalBodyContent = initialBody.content
val signatureHtml = draftSignature?.takeIf { !it.isDummy }?.content
val wrappedSignature = signatureHtml?.let { signatureUtils.encapsulateSignatureContentWithInfomaniakClass(it) }

uiSignatureLiveData.postValue(initialSignature)
uiQuoteLiveData.postValue(initialQuote)
if (isNewMessage && wrappedSignature != null) {
finalBodyContent += wrappedSignature
}

if (isNewMessage && !initialQuote.isNullOrEmpty()) {
_areQuotesVisible.emit(true)
finalBodyContent += MessageBodyUtils.encapsulateQuotesWithInfomaniakClass(initialQuote.toString())
}

val normalizedBody = normalizeHtml(finalBodyContent)
saveSnapshot(normalizedBody)

editorBodyInitializer.postValue(
BodyContentPayload(
content = normalizedBody,
type = BodyContentType.HTML_SANITIZED
)
)

if (cc.isNotEmpty() || bcc.isNotEmpty()) {
otherRecipientsFieldsAreEmpty.postValue(false)
Expand All @@ -561,6 +531,21 @@ class NewMessageViewModel @Inject constructor(
isEncryptionActivated.postValue(isEncrypted)
}

fun bodyHasPlaceholder(bodyHtml: String): Boolean {
val body = JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml).body()
return !body.hasText()
}

fun changeQuotesVisibility(areVisible: Boolean) {
_areQuotesVisible.value = areVisible
}

private fun normalizeHtml(html: String): String {
val doc = jsoupParseWithLog(html)
doc.outputSettings().prettyPrint(false)
return doc.body().html()
}

private suspend fun getLocalOrRemoteDraft(localUuid: String?): Draft? {

@Suppress("UNUSED_PARAMETER")
Expand Down Expand Up @@ -839,14 +824,6 @@ class NewMessageViewModel @Inject constructor(
if (cc.isEmpty() && bcc.isEmpty()) otherRecipientsFieldsAreEmpty.value = true
}

fun updateBodySignature(signature: Signature) {
uiSignatureLiveData.value = if (signature.isDummy) {
null
} else {
signatureUtils.encapsulateSignatureContentWithInfomaniakClass(signature.content)
}
}

fun uploadAttachmentsToServer(uiAttachments: List<Attachment>) = viewModelScope.launch(ioDispatcher) {
val localUuid = draftLocalUuid ?: return@launch
val realm = mailboxContentRealm()
Expand Down Expand Up @@ -963,8 +940,7 @@ class NewMessageViewModel @Inject constructor(
)

subject = subjectValue

body = uiBodyValue + (uiSignatureLiveData.value ?: "") + (uiQuoteLiveData.value ?: "")
body = sanitizeBody(uiBodyValue)

/**
* If we are opening for the 1st time an existing Draft created somewhere else
Expand All @@ -980,11 +956,28 @@ class NewMessageViewModel @Inject constructor(

// Only if `!isFinishing`, because if we are finishing, well… We're out of here so we don't care about all of that.
if (!isFinishing) {
copyFromRealm().saveSnapshot(uiBodyValue)
val normalizedBody = normalizeHtml(body)
copyFromRealm().saveSnapshot(normalizedBody)
isNewMessage = false
}
}

private fun sanitizeBody(html: String): String {
val doc = jsoupParseWithLog(html)
// If the user deleted the quotes text, remove the quotes div so the button to show quotes doesn't show.
val quotes = JsoupParserUtil.jsoupParseBodyFragmentWithLog(html)
.getElementsByClass(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME)
val hasQuotes = quotes.isNotEmpty()
if (hasQuotes && !quotes.hasText()) {
doc.getElementsByClass(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME).forEach { it.remove() }
}

// Don't save the draft or send the mail with quotes style display none.
if (hasQuotes) quotes.attr("style", "display: block")

return doc.html()
}

private fun Draft.updateDraftAttachmentsWithLiveData(uiAttachments: List<Attachment>, step: String) {

/**
Expand Down Expand Up @@ -1030,7 +1023,7 @@ class NewMessageViewModel @Inject constructor(
draftSnapshot.cc == ccLiveData.valueOrEmpty().toSet() &&
draftSnapshot.bcc == bccLiveData.valueOrEmpty().toSet() &&
draftSnapshot.subject == subjectValue &&
draftSnapshot.uiBody == uiBodyValue &&
sanitizeBody(draftSnapshot.uiBody) == sanitizeBody(uiBodyValue) &&
draftSnapshot.isEncrypted == isEncryptionActivated.value &&
draftSnapshot.encryptionPassword == encryptionPassword.value &&
draftSnapshot.attachmentsLocalUuids == attachmentsLiveData.valueOrEmpty()
Expand Down
19 changes: 19 additions & 0 deletions app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.infomaniak.mail.utils.UiUtils.PRIMARY_COLOR_CODE
import com.infomaniak.mail.utils.extensions.getAttributeColor
import com.infomaniak.mail.utils.extensions.loadCss
import com.infomaniak.mail.utils.extensions.readRawResource
import kotlinx.serialization.json.JsonPrimitive
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
import org.jsoup.nodes.TextNode
Expand Down Expand Up @@ -210,6 +211,11 @@ class HtmlFormatter(private val html: String) {
}
}

fun String.escapeForJS(): String {
if (isNullOrEmpty()) return this
return JsonPrimitive(this).toString().removeSurrounding("\"")
}

fun Context.getCustomDarkMode(): String = loadCss(R.raw.custom_dark_mode)

fun Context.getImproveRenderingStyle(): String = loadCss(R.raw.improve_rendering)
Expand All @@ -219,6 +225,11 @@ class HtmlFormatter(private val html: String) {
listOf(PRIMARY_COLOR_CODE to getAttributeColor(RAndroid.attr.colorPrimary)),
)

fun Context.getCustomEditorStyle(): String = loadCss(
R.raw.editor_style,
listOf(PRIMARY_COLOR_CODE to getAttributeColor(RAndroid.attr.colorPrimary))
)

fun Context.getSignatureMarginStyle(): String = loadCss(R.raw.signature_margins)

fun Context.getPrintMailStyle(): String = loadCss(R.raw.print_email)
Expand All @@ -228,6 +239,14 @@ class HtmlFormatter(private val html: String) {
listOf("MESSAGE_SELECTOR" to "#$KMAIL_MESSAGE_ID")
)

fun Context.getToggleQuotesButtonVisibilityScript(): String = loadScript(
R.raw.show_quotes_script
)

fun Context.getReplaceSignatureScript(): String = loadScript(
R.raw.replace_signature_script
)

fun Context.getFixStyleScript(): String {
return loadScript(R.raw.fix_email_style)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ object MessageBodyUtils {
"[name=\"quote\"]", // GMX
)

fun encapsulateQuotesWithInfomaniakClass(quotes: String): String {
return """<div class="$INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME" spellcheck="false">$quotes<br></div>""".trimIndent()
}

suspend fun splitContentAndQuote(body: Body): SplitBody {

val bodyContent = body.value
Expand Down
13 changes: 10 additions & 3 deletions app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.infomaniak.mail.utils

import android.content.Context
import com.infomaniak.mail.R
import com.infomaniak.mail.utils.MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_CLASS_NAME
import com.infomaniak.mail.utils.extensions.loadCss
import javax.inject.Inject
import javax.inject.Singleton
Expand All @@ -31,10 +32,16 @@ class SignatureUtils @Inject constructor(appContext: Context) {
fun encapsulateSignatureContentWithInfomaniakClass(signatureContent: String): String {
val verticalMarginsCss = signatureMargins
val verticalMarginAttributes = extractAttributesFromMarginCss(verticalMarginsCss)
return """<div class="${MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_CLASS_NAME}" style="$verticalMarginAttributes">$signatureContent</div>"""
if (verticalMarginAttributes.isNullOrEmpty()) return signatureContent

return """
<div class="$INFOMANIAK_SIGNATURE_HTML_CLASS_NAME" spellcheck="false" style="$verticalMarginAttributes">
$signatureContent
</div>
""".trimIndent()
}

private fun extractAttributesFromMarginCss(verticalMarginsCss: String): String {
return Regex("""\{(.*)\}""").find(verticalMarginsCss)!!.groupValues[1]
private fun extractAttributesFromMarginCss(verticalMarginsCss: String): String? {
return Regex("""\{(.*)\}""").find(verticalMarginsCss)?.groupValues[1]
}
}
15 changes: 1 addition & 14 deletions app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import com.infomaniak.mail.utils.HtmlFormatter.Companion.getImproveRenderingStyl
import com.infomaniak.mail.utils.HtmlFormatter.Companion.getJsBridgeScript
import com.infomaniak.mail.utils.HtmlFormatter.Companion.getPrintMailStyle
import com.infomaniak.mail.utils.HtmlFormatter.Companion.getResizeScript
import com.infomaniak.mail.utils.HtmlFormatter.Companion.getSignatureMarginStyle
import com.infomaniak.mail.utils.extensions.enableAlgorithmicDarkening
import com.infomaniak.mail.utils.extensions.loadCss

Expand All @@ -40,7 +39,6 @@ class WebViewUtils(context: Context) {
private val customDarkMode by lazy { context.getCustomDarkMode() }
private val improveRenderingStyle by lazy { context.getImproveRenderingStyle() }
private val customStyle by lazy { context.getCustomStyle() }
private val signatureVerticalMargin by lazy { context.getSignatureMarginStyle() }
private val printMailStyle by lazy { context.getPrintMailStyle() }

private val resizeScript by lazy { context.getResizeScript() }
Expand All @@ -65,15 +63,6 @@ class WebViewUtils(context: Context) {
return@with inject()
}

fun processSignatureHtmlForDisplay(
html: String,
isDisplayedInDarkMode: Boolean,
): String = with(HtmlFormatter(html)) {
addCommonDisplayContent(isDisplayedInDarkMode)
registerCss(signatureVerticalMargin)
return@with inject()
}

private fun HtmlFormatter.addCommonDisplayContent(isDisplayedInDarkMode: Boolean) {
if (isDisplayedInDarkMode) registerCss(customDarkMode, DARK_BACKGROUND_STYLE_ID)
registerCss(improveRenderingStyle)
Expand Down Expand Up @@ -134,16 +123,14 @@ class WebViewUtils(context: Context) {
private fun WebSettings.setupCommonWebViewSettings() {
@SuppressLint("SetJavaScriptEnabled")
javaScriptEnabled = true

loadWithOverviewMode = true
useWideViewPort = true

cacheMode = LOAD_CACHE_ELSE_NETWORK
}

fun WebSettings.setupThreadWebViewSettings() {
setupCommonWebViewSettings()

useWideViewPort = true
setSupportZoom(true)
builtInZoomControls = true
displayZoomControls = false
Expand Down
Loading