From 8955bb77d8b5045f0618ffcfe92b44c7e795c2c2 Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Tue, 3 Feb 2026 16:14:40 +0100 Subject: [PATCH 01/94] feat: Add signature to rich editor --- .../mail/ui/newMessage/NewMessageFragment.kt | 83 ++++++++++--------- .../mail/ui/newMessage/NewMessageViewModel.kt | 23 ++--- .../infomaniak/mail/utils/MessageBodyUtils.kt | 1 + .../infomaniak/mail/utils/SignatureUtils.kt | 4 +- .../main/res/layout/fragment_new_message.xml | 33 +------- 5 files changed, 60 insertions(+), 84 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 86559e0a00a..ac74931bddd 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -95,6 +95,7 @@ import com.infomaniak.mail.ui.newMessage.encryption.EncryptionViewModel import com.infomaniak.mail.utils.AccountUtils import com.infomaniak.mail.utils.HtmlUtils.processCids import com.infomaniak.mail.utils.JsoupParserUtil.jsoupParseWithLog +import com.infomaniak.mail.utils.MessageBodyUtils import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SignatureUtils import com.infomaniak.mail.utils.UiUtils.PRIMARY_COLOR_CODE @@ -129,6 +130,8 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import org.jsoup.Jsoup +import org.jsoup.nodes.Document import splitties.experimental.ExperimentalSplittiesApi import java.util.Date import javax.inject.Inject @@ -156,7 +159,6 @@ class NewMessageFragment : Fragment() { private var addressListPopupWindow: ListPopupWindow? = null private var quoteWebView: WebView? = null - private var signatureWebView: WebView? = null private val signatureAdapter = SignatureAdapter(::onSignatureClicked) private val attachmentAdapter inline get() = binding.attachmentsRecyclerView.adapter as AttachmentAdapter @@ -232,7 +234,7 @@ class NewMessageFragment : Fragment() { observeAttachments() observeImportAttachmentsResult() observeBodyLoader() - observeUiSignature() + observeEditorInitialization() observeUiQuote() observeShimmering() @@ -363,13 +365,9 @@ class NewMessageFragment : Fragment() { private fun setWebViewReference() { quoteWebView = binding.quoteWebView - signatureWebView = binding.signatureWebView } override fun onConfigurationChanged(newConfig: Configuration) { - newMessageViewModel.uiSignatureLiveData.value?.let { _ -> - binding.signatureWebView.reload() - } newMessageViewModel.uiQuoteLiveData.value?.let { _ -> binding.quoteWebView.reload() } @@ -385,8 +383,6 @@ class NewMessageFragment : Fragment() { addressListPopupWindow = null quoteWebView?.destroyAndClearHistory() quoteWebView = null - signatureWebView?.destroyAndClearHistory() - signatureWebView = null TransitionManager.endTransitions(binding.root) super.onDestroyView() _binding = null @@ -418,7 +414,6 @@ class NewMessageFragment : Fragment() { toolbar.setNavigationOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() } changeToolbarColorOnScroll(appBarLayout, compositionNestedScrollView) - signatureWebView.enableAlgorithmicDarkening(true) quoteWebView.enableAlgorithmicDarkening(true) attachmentsRecyclerView.adapter = AttachmentAdapter( @@ -531,21 +526,6 @@ class NewMessageFragment : Fragment() { private fun configureUiWithDraftData(draft: Draft) = with(binding) { - // Signature - signatureWebView.apply { - settings.setupNewMessageWebViewSettings() - initWebViewClientAndBridge( - attachments = emptyList(), - messageUid = "SIGNATURE-${draft.messageUid}", - shouldLoadDistantResources = true, - navigateToNewMessageActivity = null, - ) - } - removeSignature.setOnClickListener { - trackNewMessageEvent(MatomoName.DeleteSignature) - newMessageViewModel.uiSignatureLiveData.value = null - } - // Quote quoteWebView.apply { settings.setupNewMessageWebViewSettings() @@ -584,11 +564,6 @@ class NewMessageFragment : Fragment() { } } - private fun WebView.loadSignatureContent(html: String, webViewGroup: Group) { - val processedHtml = webViewUtils.processSignatureHtmlForDisplay(html, context.isNightModeEnabled()) - loadProcessedContent(processedHtml, webViewGroup) - } - private fun WebView.loadContent(html: String, webViewGroup: Group) { val processedHtml = webViewUtils.processHtmlForDisplay(html = html, isDisplayedInDarkMode = context.isNightModeEnabled()) loadProcessedContent(processedHtml, webViewGroup) @@ -632,8 +607,38 @@ class NewMessageFragment : Fragment() { private fun onSignatureClicked(signature: Signature) { trackNewMessageEvent(MatomoName.SwitchIdentity) - newMessageViewModel.fromLiveData.value = UiFrom(signature) - addressListPopupWindow?.dismiss() + + binding.editorWebView.exportHtml { html -> + val oldSignature = newMessageViewModel.fromLiveData.value?.signature?.content + + val bodyHtml = if (!oldSignature.isNullOrEmpty()) { + removeSignature(html) + } else { + html + } + + newMessageViewModel.fromLiveData.value = UiFrom(signature) + addressListPopupWindow?.dismiss() + + // Get the New Signature HTML + val newSignatureHtml = signature.content + val wrappedNewSignature = + if (signature.isDummy) "" else signatureUtils.encapsulateSignatureContentWithInfomaniakClass(newSignatureHtml) + + // Combine: New Body + New Signature + val finalHtml = bodyHtml + wrappedNewSignature + + // Update the Editor + newMessageViewModel.editorBodyInitializer.postValue(BodyContentPayload(finalHtml, BodyContentType.HTML_SANITIZED)) + } + } + + private fun removeSignature(html: String): String { + val doc: Document = Jsoup.parseBodyFragment(html).apply { + getElementById(MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_ID)?.remove() + } + + return doc.html() } private fun updateSelectedSignatureInFromField(signature: Signature) { @@ -663,9 +668,8 @@ class NewMessageFragment : Fragment() { } private fun observeFromData() = with(newMessageViewModel) { - fromLiveData.observe(viewLifecycleOwner) { (signature, shouldUpdateBodySignature) -> + fromLiveData.observe(viewLifecycleOwner) { (signature) -> updateSelectedSignatureInFromField(signature) - if (shouldUpdateBodySignature) updateBodySignature(signature) signatureAdapter.updateSelectedSignature(signature.id) } } @@ -765,13 +769,12 @@ class NewMessageFragment : Fragment() { } } - private fun observeUiSignature() = with(binding) { - newMessageViewModel.uiSignatureLiveData.observe(viewLifecycleOwner) { signature -> - if (signature == null) { - signatureGroup.isGone = true - } else { - signatureWebView.loadSignatureContent(signature, signatureGroup) - } + private fun observeEditorInitialization() { + newMessageViewModel.editorBodyInitializer.observe(viewLifecycleOwner) { bodyPayload -> + if (bodyPayload.content.isEmpty()) return@observe + + // This sets the combined Body + Signature + editorContentManager.setContent(binding.editorWebView, bodyPayload) } } 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 93a7fccd522..0c3de8a731f 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 @@ -171,7 +171,6 @@ class NewMessageViewModel @Inject constructor( val ccLiveData = MutableLiveData() val bccLiveData = MutableLiveData() val attachmentsLiveData = MutableLiveData>() - val uiSignatureLiveData = MutableLiveData() val uiQuoteLiveData = MutableLiveData() inline val allRecipients get() = toLiveData.valueOrEmpty() + ccLiveData.valueOrEmpty() + bccLiveData.valueOrEmpty() //endregion @@ -548,9 +547,19 @@ class NewMessageViewModel @Inject constructor( attachmentsLiveData.postValue(attachments) + val signatureHtml = draftSignature?.takeIf { !it.isDummy }?.content + val wrappedSignature = signatureHtml?.let { signatureUtils.encapsulateSignatureContentWithInfomaniakClass(it) } + + initialBody = BodyContentPayload( + content = when { + initialBody.content.isNotEmpty() -> initialBody.content + wrappedSignature != null -> wrappedSignature + else -> "" + }, + type = BodyContentType.HTML_UNSANITIZED + ) editorBodyInitializer.postValue(initialBody) - uiSignatureLiveData.postValue(initialSignature) uiQuoteLiveData.postValue(initialQuote) if (cc.isNotEmpty() || bcc.isNotEmpty()) { @@ -839,14 +848,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) = viewModelScope.launch(ioDispatcher) { val localUuid = draftLocalUuid ?: return@launch val realm = mailboxContentRealm() @@ -964,7 +965,7 @@ class NewMessageViewModel @Inject constructor( subject = subjectValue - body = uiBodyValue + (uiSignatureLiveData.value ?: "") + (uiQuoteLiveData.value ?: "") + body = uiBodyValue + (uiQuoteLiveData.value ?: "") /** * If we are opening for the 1st time an existing Draft created somewhere else 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 85b9f1b2692..adca8484f76 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt @@ -35,6 +35,7 @@ object MessageBodyUtils { const val INFOMANIAK_SIGNATURE_HTML_CLASS_NAME = "editorUserSignature" const val INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME = "ik_mail_quote" const val INFOMANIAK_FORWARD_QUOTE_HTML_CLASS_NAME = "forwardContentMessage" + const val INFOMANIAK_SIGNATURE_HTML_ID = "ik-signature" private const val QUOTE_DETECTION_TIMEOUT = 1_500L diff --git a/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt index 3528eee9a84..1de9ccee433 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt @@ -19,6 +19,8 @@ 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.MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_ID import com.infomaniak.mail.utils.extensions.loadCss import javax.inject.Inject import javax.inject.Singleton @@ -31,7 +33,7 @@ class SignatureUtils @Inject constructor(appContext: Context) { fun encapsulateSignatureContentWithInfomaniakClass(signatureContent: String): String { val verticalMarginsCss = signatureMargins val verticalMarginAttributes = extractAttributesFromMarginCss(verticalMarginsCss) - return """
$signatureContent
""" + return """
$signatureContent
""" } private fun extractAttributesFromMarginCss(verticalMarginsCss: String): String { diff --git a/app/src/main/res/layout/fragment_new_message.xml b/app/src/main/res/layout/fragment_new_message.xml index ea4ba1bde7c..de829f7493d 100644 --- a/app/src/main/res/layout/fragment_new_message.xml +++ b/app/src/main/res/layout/fragment_new_message.xml @@ -366,37 +366,6 @@ android:maxWidth="200dp" /> - - - - - + app:layout_constraintTop_toBottomOf="@id/editorWebView" /> Date: Wed, 11 Feb 2026 11:47:55 +0100 Subject: [PATCH 02/94] feat: Add signature and quotes to richHtlmEditorWebView --- .../mail/ui/newMessage/BodyContentPayload.kt | 8 +- .../mail/ui/newMessage/NewMessageFragment.kt | 116 ++++++++---------- .../mail/ui/newMessage/NewMessageViewModel.kt | 23 ++-- .../infomaniak/mail/utils/MessageBodyUtils.kt | 1 + app/src/main/res/drawable/ic_more_horiz.xml | 27 ++++ .../main/res/layout/fragment_new_message.xml | 33 +---- app/src/main/res/raw/editor_style.css | 11 ++ 7 files changed, 113 insertions(+), 106 deletions(-) create mode 100644 app/src/main/res/drawable/ic_more_horiz.xml 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 88f29030fda..fef59904f97 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.utils.MessageBodyUtils.INFOMANIAK_BODY_HTML_ID + /** * @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. @@ -24,7 +26,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(placeHolderText: String) = + BodyContentPayload( + content = "

$placeHolderText

", + type = BodyContentType.HTML_SANITIZED + ) } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index ac74931bddd..3937f9cf030 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -21,7 +21,6 @@ package com.infomaniak.mail.ui.newMessage import android.annotation.SuppressLint import android.app.Activity -import android.content.ClipDescription import android.content.Intent import android.content.res.Configuration import android.os.Bundle @@ -31,13 +30,11 @@ import android.transition.TransitionManager import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.webkit.WebView import android.widget.ListPopupWindow import android.widget.PopupWindow import androidx.activity.addCallback import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources -import androidx.constraintlayout.widget.Group import androidx.core.view.forEach import androidx.core.view.isGone import androidx.core.view.isVisible @@ -56,6 +53,7 @@ import com.infomaniak.core.legacy.utils.SnackbarUtils.showSnackbar import com.infomaniak.core.legacy.utils.getBackNavigationResult import com.infomaniak.core.legacy.utils.setMargins import com.infomaniak.core.ui.showToast +import com.infomaniak.lib.richhtmleditor.RichHtmlEditorWebView import com.infomaniak.lib.richhtmleditor.StatusCommand.BOLD import com.infomaniak.lib.richhtmleditor.StatusCommand.CREATE_LINK import com.infomaniak.lib.richhtmleditor.StatusCommand.ITALIC @@ -93,13 +91,10 @@ import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.UiFrom import com.infomaniak.mail.ui.newMessage.encryption.EncryptionMessageManager import com.infomaniak.mail.ui.newMessage.encryption.EncryptionViewModel import com.infomaniak.mail.utils.AccountUtils -import com.infomaniak.mail.utils.HtmlUtils.processCids -import com.infomaniak.mail.utils.JsoupParserUtil.jsoupParseWithLog import com.infomaniak.mail.utils.MessageBodyUtils import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SignatureUtils import com.infomaniak.mail.utils.UiUtils.PRIMARY_COLOR_CODE -import com.infomaniak.mail.utils.Utils import com.infomaniak.mail.utils.WebViewUtils import com.infomaniak.mail.utils.WebViewUtils.Companion.destroyAndClearHistory import com.infomaniak.mail.utils.WebViewUtils.Companion.setupNewMessageWebViewSettings @@ -125,10 +120,6 @@ import dagger.hilt.android.AndroidEntryPoint import io.sentry.Sentry import io.sentry.SentryLevel import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.jsoup.Jsoup import org.jsoup.nodes.Document @@ -151,6 +142,7 @@ class NewMessageFragment : Fragment() { private val newMessageViewModel: NewMessageViewModel by activityViewModels() private val aiViewModel: AiViewModel by activityViewModels() private val encryptionViewModel: EncryptionViewModel by activityViewModels() + private var hasPlaceholder = true private val filePicker = FilePicker(fragment = this).apply { initCallback { uris -> newMessageViewModel.importAttachmentsLiveData.value = uris } @@ -158,7 +150,7 @@ class NewMessageFragment : Fragment() { private var addressListPopupWindow: ListPopupWindow? = null - private var quoteWebView: WebView? = null + private var quoteWebView: RichHtmlEditorWebView? = null private val signatureAdapter = SignatureAdapter(::onSignatureClicked) private val attachmentAdapter inline get() = binding.attachmentsRecyclerView.adapter as AttachmentAdapter @@ -234,7 +226,6 @@ class NewMessageFragment : Fragment() { observeAttachments() observeImportAttachmentsResult() observeBodyLoader() - observeEditorInitialization() observeUiQuote() observeShimmering() @@ -463,12 +454,16 @@ class NewMessageFragment : Fragment() { private fun initEditorUi() = with(binding) { editorWebView.subscribeToStates(setOf(BOLD, ITALIC, UNDERLINE, STRIKE_THROUGH, UNORDERED_LIST, CREATE_LINK)) setEditorStyle() - handleEditorPlaceholderVisibility() editorAiAnimation.setAnimation(R.raw.euria) - setToolbarEnabledStatus(false) - disableButtonsWhenFocusIsLost() + handleFocusChanges() + } + + fun editorHasPlaceholder() = with(binding.editorWebView) { + exportHtml { html -> + hasPlaceholder = Jsoup.parseBodyFragment(html).getElementsByClass("placeholder").first() != null + } } private fun setEditorStyle() = with(binding.editorWebView) { @@ -480,19 +475,28 @@ class NewMessageFragment : Fragment() { addCss(context.loadCss(R.raw.editor_style, customColors)) } - private fun handleEditorPlaceholderVisibility() { - val isPlaceholderVisible = combine( - binding.editorWebView.isEmptyFlow.filterNotNull(), - newMessageViewModel.isShimmering, - ) { isEditorEmpty, isShimmering -> isEditorEmpty && !isShimmering } + private fun removePlaceholder() = with(binding.editorWebView) { + exportHtml { html -> + val doc: Document = Jsoup.parseBodyFragment(html).apply { + getElementsByClass("placeholder").first()?.text("")?.removeClass("placeholder") + } - isPlaceholderVisible - .onEach { isVisible -> binding.newMessagePlaceholder.isVisible = isVisible } - .launchIn(lifecycleScope) + newMessageViewModel.editorBodyInitializer.postValue( + BodyContentPayload( + doc.html(), + BodyContentType.HTML_SANITIZED + ) + ) + } } - private fun disableButtonsWhenFocusIsLost() { - newMessageViewModel.isEditorWebViewFocusedLiveData.observe(viewLifecycleOwner, ::setToolbarEnabledStatus) + private fun handleFocusChanges() { + newMessageViewModel.isEditorWebViewFocusedLiveData.observe(viewLifecycleOwner) { isFocused -> + setToolbarEnabledStatus(isFocused) + if (isFocused && hasPlaceholder) { + removePlaceholder() + } + } } private fun setToolbarEnabledStatus(isEnabled: Boolean) { @@ -537,41 +541,18 @@ class NewMessageFragment : Fragment() { navigateToNewMessageActivity = null, ) } + removeQuote.setOnClickListener { trackNewMessageEvent(MatomoName.DeleteQuote) - removeInlineAttachmentsUsedInQuote() - newMessageViewModel.uiQuoteLiveData.value = null + quoteWebView.isVisible = !quoteWebView.isVisible } } - private fun removeInlineAttachmentsUsedInQuote() = with(newMessageViewModel) { - uiQuoteLiveData.value?.let { html -> - attachmentsLiveData.value?.filterOutHtmlCids(html)?.let { attachmentsLiveData.value = it } - } - } - - private fun List.filterOutHtmlCids(html: String): List { - return buildList { - addAll(this@filterOutHtmlCids) - - jsoupParseWithLog(html).processCids( - attachments = this@filterOutHtmlCids, - associateDataToCid = { it }, - onCidImageFound = { attachment, _ -> - remove(attachment) - } - ) - } - } - - private fun WebView.loadContent(html: String, webViewGroup: Group) { + private fun RichHtmlEditorWebView.loadContent(html: String) { val processedHtml = webViewUtils.processHtmlForDisplay(html = html, isDisplayedInDarkMode = context.isNightModeEnabled()) - loadProcessedContent(processedHtml, webViewGroup) - } - - private fun WebView.loadProcessedContent(processedHtml: String, webViewGroup: Group) { - webViewGroup.isVisible = processedHtml.isNotBlank() - loadDataWithBaseURL("", processedHtml, ClipDescription.MIMETYPE_TEXT_HTML, Utils.UTF_8, "") + quoteWebView?.isVisible = false + binding.removeQuote.isVisible = processedHtml.isNotBlank() + setHtml(processedHtml) } private fun setupFromField(signatures: List) = with(binding) { @@ -609,12 +590,13 @@ class NewMessageFragment : Fragment() { trackNewMessageEvent(MatomoName.SwitchIdentity) binding.editorWebView.exportHtml { html -> + val body = Jsoup.parse(html).body() val oldSignature = newMessageViewModel.fromLiveData.value?.signature?.content val bodyHtml = if (!oldSignature.isNullOrEmpty()) { - removeSignature(html) + removeSignature(body.html()) } else { - html + body.html() } newMessageViewModel.fromLiveData.value = UiFrom(signature) @@ -626,13 +608,21 @@ class NewMessageFragment : Fragment() { if (signature.isDummy) "" else signatureUtils.encapsulateSignatureContentWithInfomaniakClass(newSignatureHtml) // Combine: New Body + New Signature - val finalHtml = bodyHtml + wrappedNewSignature + val finalHtml = addSignatureInsideBody(bodyHtml, wrappedNewSignature) // Update the Editor newMessageViewModel.editorBodyInitializer.postValue(BodyContentPayload(finalHtml, BodyContentType.HTML_SANITIZED)) } } + private fun addSignatureInsideBody(bodyHtml: String, wrappedSignature: String): String { + if (wrappedSignature.isEmpty()) return bodyHtml + + val doc = Jsoup.parseBodyFragment(bodyHtml) + doc.body().append(wrappedSignature) + return doc.body().html() + } + private fun removeSignature(html: String): String { val doc: Document = Jsoup.parseBodyFragment(html).apply { getElementById(MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_ID)?.remove() @@ -766,15 +756,7 @@ class NewMessageFragment : Fragment() { private fun observeBodyLoader() { newMessageViewModel.editorBodyInitializer.observe(viewLifecycleOwner) { body -> editorContentManager.setContent(binding.editorWebView, body) - } - } - - private fun observeEditorInitialization() { - newMessageViewModel.editorBodyInitializer.observe(viewLifecycleOwner) { bodyPayload -> - if (bodyPayload.content.isEmpty()) return@observe - - // This sets the combined Body + Signature - editorContentManager.setContent(binding.editorWebView, bodyPayload) + editorHasPlaceholder() } } @@ -783,7 +765,7 @@ class NewMessageFragment : Fragment() { if (quote == null) { quoteGroup.isGone = true } else { - quoteWebView.loadContent(quote, quoteGroup) + quoteWebView.loadContent(quote) } } } 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 0c3de8a731f..157ecd6bc8b 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 @@ -24,6 +24,7 @@ import android.content.ClipDescription import android.content.Intent import android.net.Uri import android.os.Parcelable +import android.util.Log import androidx.core.app.NotificationManagerCompat import androidx.core.net.MailTo import androidx.core.net.toUri @@ -130,6 +131,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.invoke import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.jsoup.Jsoup import org.jsoup.nodes.Document import splitties.experimental.ExperimentalSplittiesApi import java.util.Date @@ -160,7 +162,8 @@ class NewMessageViewModel @Inject constructor( private val ioCoroutineContext = viewModelScope.coroutineContext(ioDispatcher) //region Initial data - private var initialBody: BodyContentPayload = BodyContentPayload.emptyBody() + private var initialBody: BodyContentPayload = + BodyContentPayload.emptyBody(appContext.getString(R.string.newMessagePlaceholderTitle)) private var initialSignature: String? = null private var initialQuote: String? = null //endregion @@ -549,15 +552,13 @@ class NewMessageViewModel @Inject constructor( val signatureHtml = draftSignature?.takeIf { !it.isDummy }?.content val wrappedSignature = signatureHtml?.let { signatureUtils.encapsulateSignatureContentWithInfomaniakClass(it) } - + val bodyHasSignature = bodyHasSignature(initialBody.content) initialBody = BodyContentPayload( - content = when { - initialBody.content.isNotEmpty() -> initialBody.content - wrappedSignature != null -> wrappedSignature - else -> "" - }, - type = BodyContentType.HTML_UNSANITIZED + content = if (bodyHasSignature || wrappedSignature == null) initialBody.content else initialBody.content + wrappedSignature, + type = BodyContentType.HTML_SANITIZED ) + + Log.d("HTLM initial body", initialBody.content) editorBodyInitializer.postValue(initialBody) uiQuoteLiveData.postValue(initialQuote) @@ -570,6 +571,10 @@ class NewMessageViewModel @Inject constructor( isEncryptionActivated.postValue(isEncrypted) } + fun bodyHasSignature(bodyHtml: String): Boolean { + return Jsoup.parseBodyFragment(bodyHtml).getElementById(MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_ID) != null + } + private suspend fun getLocalOrRemoteDraft(localUuid: String?): Draft? { @Suppress("UNUSED_PARAMETER") @@ -705,7 +710,7 @@ class NewMessageViewModel @Inject constructor( val bodyContent = mailToIntent?.body?.takeIf(String::isNotEmpty) ?: intent?.getStringExtra(Intent.EXTRA_TEXT) val mailToPayload = bodyContent?.let { BodyContentPayload(it, BodyContentType.TEXT_PLAIN_WITH_HTML) } - initialBody = mailToPayload ?: BodyContentPayload.emptyBody() + initialBody = mailToPayload ?: BodyContentPayload.emptyBody(appContext.getString(R.string.newMessagePlaceholderTitle)) } } 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 adca8484f76..3ebb4e9bd38 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt @@ -36,6 +36,7 @@ object MessageBodyUtils { const val INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME = "ik_mail_quote" const val INFOMANIAK_FORWARD_QUOTE_HTML_CLASS_NAME = "forwardContentMessage" const val INFOMANIAK_SIGNATURE_HTML_ID = "ik-signature" + const val INFOMANIAK_BODY_HTML_ID = "ik-body" private const val QUOTE_DETECTION_TIMEOUT = 1_500L diff --git a/app/src/main/res/drawable/ic_more_horiz.xml b/app/src/main/res/drawable/ic_more_horiz.xml new file mode 100644 index 00000000000..41bca2ea6b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_horiz.xml @@ -0,0 +1,27 @@ + + + + diff --git a/app/src/main/res/layout/fragment_new_message.xml b/app/src/main/res/layout/fragment_new_message.xml index de829f7493d..70854415f28 100644 --- a/app/src/main/res/layout/fragment_new_message.xml +++ b/app/src/main/res/layout/fragment_new_message.xml @@ -302,23 +302,6 @@ app:layout_constraintTop_toBottomOf="@id/attachmentsRecyclerView" tools:layout_height="100dp" /> - - - - - Date: Thu, 12 Feb 2026 09:15:49 +0100 Subject: [PATCH 03/94] feat: Add everything into the mainwebview --- .../mail/ui/newMessage/BodyContentPayload.kt | 2 +- .../mail/ui/newMessage/NewMessageFragment.kt | 67 ++----------------- .../mail/ui/newMessage/NewMessageViewModel.kt | 13 +++- app/src/main/res/drawable/ic_more_horiz.xml | 4 +- .../main/res/layout/fragment_new_message.xml | 33 --------- app/src/main/res/raw/editor_style.css | 3 +- 6 files changed, 22 insertions(+), 100 deletions(-) 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 fef59904f97..98793d17578 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 @@ -28,7 +28,7 @@ data class BodyContentPayload(val content: String, val type: BodyContentType) { companion object { fun emptyBody(placeHolderText: String) = BodyContentPayload( - content = "

$placeHolderText

", + content = "

$placeHolderText


", type = BodyContentType.HTML_SANITIZED ) } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 3937f9cf030..812af17c1d8 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -66,11 +66,9 @@ import com.infomaniak.mail.MatomoMail.trackNewMessageEvent import com.infomaniak.mail.MatomoMail.trackScheduleSendEvent import com.infomaniak.mail.R import com.infomaniak.mail.data.LocalSettings -import com.infomaniak.mail.data.LocalSettings.ExternalContent import com.infomaniak.mail.data.models.Attachment import com.infomaniak.mail.data.models.AttachmentDisposition import com.infomaniak.mail.data.models.FeatureFlag -import com.infomaniak.mail.data.models.draft.Draft import com.infomaniak.mail.data.models.draft.Draft.DraftAction import com.infomaniak.mail.data.models.draft.Draft.DraftMode import com.infomaniak.mail.data.models.mailbox.Mailbox @@ -95,9 +93,7 @@ import com.infomaniak.mail.utils.MessageBodyUtils import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SignatureUtils import com.infomaniak.mail.utils.UiUtils.PRIMARY_COLOR_CODE -import com.infomaniak.mail.utils.WebViewUtils import com.infomaniak.mail.utils.WebViewUtils.Companion.destroyAndClearHistory -import com.infomaniak.mail.utils.WebViewUtils.Companion.setupNewMessageWebViewSettings import com.infomaniak.mail.utils.extensions.AttachmentExt import com.infomaniak.mail.utils.extensions.AttachmentExt.openAttachment import com.infomaniak.mail.utils.extensions.applySideAndBottomSystemInsets @@ -108,7 +104,6 @@ import com.infomaniak.mail.utils.extensions.changeToolbarColorOnScroll import com.infomaniak.mail.utils.extensions.enableAlgorithmicDarkening import com.infomaniak.mail.utils.extensions.getAttributeColor import com.infomaniak.mail.utils.extensions.ime -import com.infomaniak.mail.utils.extensions.initWebViewClientAndBridge import com.infomaniak.mail.utils.extensions.loadCss import com.infomaniak.mail.utils.extensions.navigateToDownloadProgressDialog import com.infomaniak.mail.utils.extensions.systemBars @@ -156,7 +151,6 @@ class NewMessageFragment : Fragment() { private val attachmentAdapter inline get() = binding.attachmentsRecyclerView.adapter as AttachmentAdapter private val newMessageActivity by lazy { requireActivity() as NewMessageActivity } - private val webViewUtils by lazy { WebViewUtils(requireContext()) } @Inject lateinit var editorContentManager: EditorContentManager @@ -213,7 +207,6 @@ class NewMessageFragment : Fragment() { bindAlertToViewLifecycle(descriptionDialog) - setWebViewReference() initMailbox() initUi() initializeDraft() @@ -226,7 +219,6 @@ class NewMessageFragment : Fragment() { observeAttachments() observeImportAttachmentsResult() observeBodyLoader() - observeUiQuote() observeShimmering() setupBackActionHandler() @@ -354,14 +346,8 @@ class NewMessageFragment : Fragment() { ) } - private fun setWebViewReference() { - quoteWebView = binding.quoteWebView - } - override fun onConfigurationChanged(newConfig: Configuration) { - newMessageViewModel.uiQuoteLiveData.value?.let { _ -> - binding.quoteWebView.reload() - } + //TODO: CHECK IF WE SHOULD ADD A RELOD HERE super.onConfigurationChanged(newConfig) } @@ -405,8 +391,6 @@ class NewMessageFragment : Fragment() { toolbar.setNavigationOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() } changeToolbarColorOnScroll(appBarLayout, compositionNestedScrollView) - quoteWebView.enableAlgorithmicDarkening(true) - attachmentsRecyclerView.adapter = AttachmentAdapter( shouldDisplayCloseButton = true, onDelete = ::onDeleteAttachment, @@ -453,6 +437,7 @@ class NewMessageFragment : Fragment() { private fun initEditorUi() = with(binding) { editorWebView.subscribeToStates(setOf(BOLD, ITALIC, UNDERLINE, STRIKE_THROUGH, UNORDERED_LIST, CREATE_LINK)) + editorWebView.withSpellCheck(false) setEditorStyle() editorAiAnimation.setAnimation(R.raw.euria) @@ -528,33 +513,6 @@ class NewMessageFragment : Fragment() { } } - private fun configureUiWithDraftData(draft: Draft) = with(binding) { - - // Quote - quoteWebView.apply { - settings.setupNewMessageWebViewSettings() - val alwaysShowExternalContent = localSettings.externalContent == ExternalContent.ALWAYS - initWebViewClientAndBridge( - attachments = draft.attachments, - messageUid = "QUOTE-${draft.messageUid}", - shouldLoadDistantResources = alwaysShowExternalContent || newMessageViewModel.shouldLoadDistantResources(), - navigateToNewMessageActivity = null, - ) - } - - removeQuote.setOnClickListener { - trackNewMessageEvent(MatomoName.DeleteQuote) - quoteWebView.isVisible = !quoteWebView.isVisible - } - } - - private fun RichHtmlEditorWebView.loadContent(html: String) { - val processedHtml = webViewUtils.processHtmlForDisplay(html = html, isDisplayedInDarkMode = context.isNightModeEnabled()) - quoteWebView?.isVisible = false - binding.removeQuote.isVisible = processedHtml.isNotBlank() - setHtml(processedHtml) - } - private fun setupFromField(signatures: List) = with(binding) { signatureAdapter.setList(signatures) @@ -608,18 +566,18 @@ class NewMessageFragment : Fragment() { if (signature.isDummy) "" else signatureUtils.encapsulateSignatureContentWithInfomaniakClass(newSignatureHtml) // Combine: New Body + New Signature - val finalHtml = addSignatureInsideBody(bodyHtml, wrappedNewSignature) + val finalHtml = addInsideBody(bodyHtml, wrappedNewSignature) // Update the Editor newMessageViewModel.editorBodyInitializer.postValue(BodyContentPayload(finalHtml, BodyContentType.HTML_SANITIZED)) } } - private fun addSignatureInsideBody(bodyHtml: String, wrappedSignature: String): String { - if (wrappedSignature.isEmpty()) return bodyHtml + private fun addInsideBody(bodyHtml: String, element: String): String { + if (element.isEmpty()) return bodyHtml val doc = Jsoup.parseBodyFragment(bodyHtml) - doc.body().append(wrappedSignature) + doc.body().append(element) return doc.body().html() } @@ -649,8 +607,7 @@ class NewMessageFragment : Fragment() { } private fun observeInitResult() { - newMessageViewModel.initResult.observe(viewLifecycleOwner) { (draft, signatures) -> - configureUiWithDraftData(draft) + newMessageViewModel.initResult.observe(viewLifecycleOwner) { (_, signatures) -> setupFromField(signatures) editorManager.setupEditorFormatActions() editorManager.setupEditorFormatActionsToggle() @@ -760,16 +717,6 @@ class NewMessageFragment : Fragment() { } } - private fun observeUiQuote() = with(binding) { - newMessageViewModel.uiQuoteLiveData.observe(viewLifecycleOwner) { quote -> - if (quote == null) { - quoteGroup.isGone = true - } else { - quoteWebView.loadContent(quote) - } - } - } - private fun observeShimmering() = lifecycleScope.launch { newMessageViewModel.isShimmering.collect(::setShimmerVisibility) } 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 157ecd6bc8b..2bbf2df0154 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 @@ -24,7 +24,6 @@ import android.content.ClipDescription import android.content.Intent import android.net.Uri import android.os.Parcelable -import android.util.Log import androidx.core.app.NotificationManagerCompat import androidx.core.net.MailTo import androidx.core.net.toUri @@ -553,12 +552,20 @@ class NewMessageViewModel @Inject constructor( val signatureHtml = draftSignature?.takeIf { !it.isDummy }?.content val wrappedSignature = signatureHtml?.let { signatureUtils.encapsulateSignatureContentWithInfomaniakClass(it) } val bodyHasSignature = bodyHasSignature(initialBody.content) + var bodyContent = initialBody.content + + if (!bodyHasSignature && wrappedSignature != null) { + bodyContent = initialBody.content + wrappedSignature + } + if (initialQuote != null) { + bodyContent += initialQuote + } + initialBody = BodyContentPayload( - content = if (bodyHasSignature || wrappedSignature == null) initialBody.content else initialBody.content + wrappedSignature, + content = bodyContent, type = BodyContentType.HTML_SANITIZED ) - Log.d("HTLM initial body", initialBody.content) editorBodyInitializer.postValue(initialBody) uiQuoteLiveData.postValue(initialQuote) diff --git a/app/src/main/res/drawable/ic_more_horiz.xml b/app/src/main/res/drawable/ic_more_horiz.xml index 41bca2ea6b8..c4d35ea4975 100644 --- a/app/src/main/res/drawable/ic_more_horiz.xml +++ b/app/src/main/res/drawable/ic_more_horiz.xml @@ -16,8 +16,8 @@ ~ along with this program. If not, see . --> diff --git a/app/src/main/res/layout/fragment_new_message.xml b/app/src/main/res/layout/fragment_new_message.xml index 70854415f28..0540ff656f0 100644 --- a/app/src/main/res/layout/fragment_new_message.xml +++ b/app/src/main/res/layout/fragment_new_message.xml @@ -349,39 +349,6 @@ android:maxWidth="200dp" />
- - - - - - Date: Thu, 12 Feb 2026 16:17:19 +0100 Subject: [PATCH 04/94] feat: Add everything in the same webview and add button to hide quotes --- .../mail/ui/newMessage/BodyContentPayload.kt | 2 +- .../mail/ui/newMessage/NewMessageFragment.kt | 13 ++-- .../mail/ui/newMessage/NewMessageViewModel.kt | 62 +++++++++++++------ .../infomaniak/mail/utils/MessageBodyUtils.kt | 7 ++- app/src/main/res/raw/editor_style.css | 3 +- .../res/raw/toggle_quote_visibility_script.js | 34 ++++++++++ 6 files changed, 93 insertions(+), 28 deletions(-) create mode 100644 app/src/main/res/raw/toggle_quote_visibility_script.js 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 98793d17578..fef59904f97 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 @@ -28,7 +28,7 @@ data class BodyContentPayload(val content: String, val type: BodyContentType) { companion object { fun emptyBody(placeHolderText: String) = BodyContentPayload( - content = "

$placeHolderText


", + content = "

$placeHolderText

", type = BodyContentType.HTML_SANITIZED ) } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 812af17c1d8..41246af9a22 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -27,6 +27,7 @@ import android.os.Bundle import android.text.InputFilter import android.text.Spanned import android.transition.TransitionManager +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -53,7 +54,6 @@ import com.infomaniak.core.legacy.utils.SnackbarUtils.showSnackbar import com.infomaniak.core.legacy.utils.getBackNavigationResult import com.infomaniak.core.legacy.utils.setMargins import com.infomaniak.core.ui.showToast -import com.infomaniak.lib.richhtmleditor.RichHtmlEditorWebView import com.infomaniak.lib.richhtmleditor.StatusCommand.BOLD import com.infomaniak.lib.richhtmleditor.StatusCommand.CREATE_LINK import com.infomaniak.lib.richhtmleditor.StatusCommand.ITALIC @@ -93,7 +93,6 @@ import com.infomaniak.mail.utils.MessageBodyUtils import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SignatureUtils import com.infomaniak.mail.utils.UiUtils.PRIMARY_COLOR_CODE -import com.infomaniak.mail.utils.WebViewUtils.Companion.destroyAndClearHistory import com.infomaniak.mail.utils.extensions.AttachmentExt import com.infomaniak.mail.utils.extensions.AttachmentExt.openAttachment import com.infomaniak.mail.utils.extensions.applySideAndBottomSystemInsets @@ -106,6 +105,7 @@ import com.infomaniak.mail.utils.extensions.getAttributeColor import com.infomaniak.mail.utils.extensions.ime import com.infomaniak.mail.utils.extensions.loadCss import com.infomaniak.mail.utils.extensions.navigateToDownloadProgressDialog +import com.infomaniak.mail.utils.extensions.readRawResource import com.infomaniak.mail.utils.extensions.systemBars import com.infomaniak.mail.utils.extensions.valueOrEmpty import com.infomaniak.mail.utils.openKSuiteProBottomSheet @@ -145,8 +145,6 @@ class NewMessageFragment : Fragment() { private var addressListPopupWindow: ListPopupWindow? = null - private var quoteWebView: RichHtmlEditorWebView? = null - private val signatureAdapter = SignatureAdapter(::onSignatureClicked) private val attachmentAdapter inline get() = binding.attachmentsRecyclerView.adapter as AttachmentAdapter @@ -358,8 +356,6 @@ class NewMessageFragment : Fragment() { } addressListPopupWindow = null - quoteWebView?.destroyAndClearHistory() - quoteWebView = null TransitionManager.endTransitions(binding.root) super.onDestroyView() _binding = null @@ -447,7 +443,7 @@ class NewMessageFragment : Fragment() { fun editorHasPlaceholder() = with(binding.editorWebView) { exportHtml { html -> - hasPlaceholder = Jsoup.parseBodyFragment(html).getElementsByClass("placeholder").first() != null + hasPlaceholder = newMessageViewModel.bodyHasPlaceholder(html) } } @@ -548,6 +544,7 @@ class NewMessageFragment : Fragment() { trackNewMessageEvent(MatomoName.SwitchIdentity) binding.editorWebView.exportHtml { html -> + Log.d("HTML", html) val body = Jsoup.parse(html).body() val oldSignature = newMessageViewModel.fromLiveData.value?.signature?.content @@ -713,6 +710,8 @@ class NewMessageFragment : Fragment() { private fun observeBodyLoader() { newMessageViewModel.editorBodyInitializer.observe(viewLifecycleOwner) { body -> editorContentManager.setContent(binding.editorWebView, body) + val script = context?.readRawResource(R.raw.toggle_quote_visibility_script) + script?.let { binding.editorWebView.addScript(script) } editorHasPlaceholder() } } 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 2bbf2df0154..e850992473b 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 @@ -24,6 +24,7 @@ import android.content.ClipDescription import android.content.Intent import android.net.Uri import android.os.Parcelable +import android.util.Log import androidx.core.app.NotificationManagerCompat import androidx.core.net.MailTo import androidx.core.net.toUri @@ -173,7 +174,6 @@ class NewMessageViewModel @Inject constructor( val ccLiveData = MutableLiveData() val bccLiveData = MutableLiveData() val attachmentsLiveData = MutableLiveData>() - val uiQuoteLiveData = MutableLiveData() inline val allRecipients get() = toLiveData.valueOrEmpty() + ccLiveData.valueOrEmpty() + bccLiveData.valueOrEmpty() //endregion @@ -549,26 +549,30 @@ class NewMessageViewModel @Inject constructor( attachmentsLiveData.postValue(attachments) + var finalBodyContent = initialBody.content val signatureHtml = draftSignature?.takeIf { !it.isDummy }?.content val wrappedSignature = signatureHtml?.let { signatureUtils.encapsulateSignatureContentWithInfomaniakClass(it) } - val bodyHasSignature = bodyHasSignature(initialBody.content) - var bodyContent = initialBody.content - if (!bodyHasSignature && wrappedSignature != null) { - bodyContent = initialBody.content + wrappedSignature - } - if (initialQuote != null) { - bodyContent += initialQuote + if (isNewMessage && wrappedSignature != null) { + finalBodyContent += wrappedSignature + } else if (!initialSignature.isNullOrEmpty()) { + finalBodyContent += initialSignature } - initialBody = BodyContentPayload( - content = bodyContent, - type = BodyContentType.HTML_SANITIZED - ) + if (!initialQuote.isNullOrEmpty()) { + finalBodyContent += buildQuoteToggleButton() + finalBodyContent += MessageBodyUtils.encapsulateQuotesWithInfomaniakClass(initialQuote.toString()) - editorBodyInitializer.postValue(initialBody) + } - uiQuoteLiveData.postValue(initialQuote) + Log.d("BODY", finalBodyContent) + + editorBodyInitializer.postValue( + BodyContentPayload( + content = finalBodyContent, + type = BodyContentType.HTML_SANITIZED + ) + ) if (cc.isNotEmpty() || bcc.isNotEmpty()) { otherRecipientsFieldsAreEmpty.postValue(false) @@ -578,8 +582,31 @@ class NewMessageViewModel @Inject constructor( isEncryptionActivated.postValue(isEncrypted) } - fun bodyHasSignature(bodyHtml: String): Boolean { - return Jsoup.parseBodyFragment(bodyHtml).getElementById(MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_ID) != null + fun bodyHasPlaceholder(bodyHtml: String): Boolean { + return Jsoup.parseBodyFragment(bodyHtml).getElementsByClass("placeholder").first() != null + } + + private fun buildQuoteToggleButton(): String { + return """ + + """.trimIndent() } private suspend fun getLocalOrRemoteDraft(localUuid: String?): Draft? { @@ -976,8 +1003,7 @@ class NewMessageViewModel @Inject constructor( ) subject = subjectValue - - body = uiBodyValue + (uiQuoteLiveData.value ?: "") + body = uiBodyValue /** * If we are opening for the 1st time an existing Draft created somewhere else 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 3ebb4e9bd38..bf0e97beed7 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt @@ -35,8 +35,9 @@ object MessageBodyUtils { const val INFOMANIAK_SIGNATURE_HTML_CLASS_NAME = "editorUserSignature" const val INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME = "ik_mail_quote" const val INFOMANIAK_FORWARD_QUOTE_HTML_CLASS_NAME = "forwardContentMessage" - const val INFOMANIAK_SIGNATURE_HTML_ID = "ik-signature" const val INFOMANIAK_BODY_HTML_ID = "ik-body" + const val INFOMANIAK_SIGNATURE_HTML_ID = "ik-signature" + const val INFOMANIAK_QUOTES_HTML_ID = "ik-quotes" private const val QUOTE_DETECTION_TIMEOUT = 1_500L @@ -65,6 +66,10 @@ object MessageBodyUtils { "[name=\"quote\"]", // GMX ) + fun encapsulateQuotesWithInfomaniakClass(quotes: String): String { + return """
$quotes
""" + } + suspend fun splitContentAndQuote(body: Body): SplitBody { val bodyContent = body.value diff --git a/app/src/main/res/raw/editor_style.css b/app/src/main/res/raw/editor_style.css index 65ed7a60936..53ddda9d22c 100644 --- a/app/src/main/res/raw/editor_style.css +++ b/app/src/main/res/raw/editor_style.css @@ -16,5 +16,6 @@ p { .placeholder{ color: #b3b3b3; - } + + diff --git a/app/src/main/res/raw/toggle_quote_visibility_script.js b/app/src/main/res/raw/toggle_quote_visibility_script.js new file mode 100644 index 00000000000..11acb23a738 --- /dev/null +++ b/app/src/main/res/raw/toggle_quote_visibility_script.js @@ -0,0 +1,34 @@ +(function() { + function changeQuoteVisibility() { + // Hide quotes initially + const quoteElements = document.querySelectorAll('[id="ik-quotes"]'); + quoteElements.forEach(el => { + el.style.display = 'none'; + }); + + // Attach click handler to button + const button = document.getElementById('quote-toggle-btn'); + + if (button) { + button.onclick = function(e) { + e.preventDefault(); + const quoteElements = document.querySelectorAll('[id="ik-quotes"]'); + quoteElements.forEach(el => { + el.style.display = 'block'; + }); + button.style.display = 'none'; + }; + +// button.addEventListener('click', function(e) { +// e.preventDefault(); +// const quoteElements = document.querySelectorAll('[id="ik-quotes"]'); +// quoteElements.forEach(el => { +// el.style.display = 'block'; +// }); +// button.style.display = 'none'; +// }); + } + } + + changeQuoteVisibility(); +})(); From f7a6f361a9597cb56881484fbb4c5b090a2a0322 Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Fri, 13 Feb 2026 16:07:12 +0100 Subject: [PATCH 05/94] feat: Changes in quote toggle button and in webview focus --- .../mail/ui/newMessage/BodyContentPayload.kt | 2 +- .../mail/ui/newMessage/NewMessageFragment.kt | 9 ++-- .../mail/ui/newMessage/NewMessageViewModel.kt | 44 ++++++++----------- .../infomaniak/mail/utils/MessageBodyUtils.kt | 14 +++++- .../infomaniak/mail/utils/SignatureUtils.kt | 2 +- app/src/main/res/raw/style.css | 23 ++++++++++ .../res/raw/toggle_quote_visibility_script.js | 36 +++++---------- 7 files changed, 71 insertions(+), 59 deletions(-) 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 fef59904f97..98793d17578 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 @@ -28,7 +28,7 @@ data class BodyContentPayload(val content: String, val type: BodyContentType) { companion object { fun emptyBody(placeHolderText: String) = BodyContentPayload( - content = "

$placeHolderText

", + content = "

$placeHolderText


", type = BodyContentType.HTML_SANITIZED ) } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 41246af9a22..f4c376d062f 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -433,9 +433,7 @@ class NewMessageFragment : Fragment() { private fun initEditorUi() = with(binding) { editorWebView.subscribeToStates(setOf(BOLD, ITALIC, UNDERLINE, STRIKE_THROUGH, UNORDERED_LIST, CREATE_LINK)) - editorWebView.withSpellCheck(false) setEditorStyle() - editorAiAnimation.setAnimation(R.raw.euria) setToolbarEnabledStatus(false) handleFocusChanges() @@ -488,7 +486,8 @@ class NewMessageFragment : Fragment() { if (initResult.value == null) { initDraftAndViewModel(intent = requireActivity().intent).observe(viewLifecycleOwner) { draft -> if (draft != null) { - showKeyboardInCorrectView(isToFieldEmpty = draft.to.isEmpty()) + val isBodyEmpty = newMessageViewModel.bodyHasPlaceholder(draft.body) + showKeyboardInCorrectView(isToFieldEmpty = draft.to.isEmpty(), isBodyEmpty = isBodyEmpty) binding.subjectTextField.setText(draft.subject) } else { requireActivity().apply { @@ -500,10 +499,10 @@ class NewMessageFragment : Fragment() { } } - private fun showKeyboardInCorrectView(isToFieldEmpty: Boolean) = with(recipientFieldsManager) { + private fun showKeyboardInCorrectView(isToFieldEmpty: Boolean, isBodyEmpty: Boolean) = with(recipientFieldsManager) { when (newMessageViewModel.draftMode()) { DraftMode.REPLY, - DraftMode.REPLY_ALL -> focusBodyField() + DraftMode.REPLY_ALL -> if (isBodyEmpty) focusBodyField() DraftMode.FORWARD -> focusToField() DraftMode.NEW_MAIL -> if (isToFieldEmpty) focusToField() else focusBodyField() } 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 e850992473b..77d2692882f 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 @@ -87,6 +87,7 @@ import com.infomaniak.mail.utils.DraftInitManager 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.INFOMANIAK_QUOTES_HTML_ID import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SharedUtils import com.infomaniak.mail.utils.SignatureUtils @@ -559,10 +560,13 @@ class NewMessageViewModel @Inject constructor( finalBodyContent += initialSignature } + Log.d("INITIAL QUOTE", initialQuote.toString()) if (!initialQuote.isNullOrEmpty()) { - finalBodyContent += buildQuoteToggleButton() - finalBodyContent += MessageBodyUtils.encapsulateQuotesWithInfomaniakClass(initialQuote.toString()) - + if (!bodyHasQuotes(initialQuote.toString())) { + finalBodyContent += MessageBodyUtils.encapsulateQuotesWithInfomaniakClass(initialQuote.toString()) + } else { + finalBodyContent += initialQuote + } } Log.d("BODY", finalBodyContent) @@ -586,27 +590,9 @@ class NewMessageViewModel @Inject constructor( return Jsoup.parseBodyFragment(bodyHtml).getElementsByClass("placeholder").first() != null } - private fun buildQuoteToggleButton(): String { - return """ - - """.trimIndent() + fun bodyHasQuotes(bodyHtml: String): Boolean { + Log.d("SIGNATURE HTML", bodyHtml) + return Jsoup.parseBodyFragment(bodyHtml).getElementById(INFOMANIAK_QUOTES_HTML_ID) != null } private suspend fun getLocalOrRemoteDraft(localUuid: String?): Draft? { @@ -1003,7 +989,7 @@ class NewMessageViewModel @Inject constructor( ) subject = subjectValue - body = uiBodyValue + body = sanitizeBodyBeforeSaving(uiBodyValue) /** * If we are opening for the 1st time an existing Draft created somewhere else @@ -1024,6 +1010,14 @@ class NewMessageViewModel @Inject constructor( } } + private fun sanitizeBodyBeforeSaving(html: String): String { + val doc = jsoupParseWithLog(html) + // Remove the toggle button before saving + doc.getElementById("quote-toggle-btn")?.remove() + return doc.html() + } + + private fun Draft.updateDraftAttachmentsWithLiveData(uiAttachments: List, step: String) { /** 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 bf0e97beed7..6866584fe16 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt @@ -67,7 +67,19 @@ object MessageBodyUtils { ) fun encapsulateQuotesWithInfomaniakClass(quotes: String): String { - return """
$quotes
""" + return """
${toggleButton()}
""" + } + + fun toggleButton(): String { + return """ + + """.trimIndent() } suspend fun splitContentAndQuote(body: Body): SplitBody { diff --git a/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt index 1de9ccee433..99b89f08d85 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt @@ -33,7 +33,7 @@ class SignatureUtils @Inject constructor(appContext: Context) { fun encapsulateSignatureContentWithInfomaniakClass(signatureContent: String): String { val verticalMarginsCss = signatureMargins val verticalMarginAttributes = extractAttributesFromMarginCss(verticalMarginsCss) - return """
$signatureContent
""" + return """
$signatureContent
""" } private fun extractAttributesFromMarginCss(verticalMarginsCss: String): String { diff --git a/app/src/main/res/raw/style.css b/app/src/main/res/raw/style.css index c7eb27097b6..310584792c3 100644 --- a/app/src/main/res/raw/style.css +++ b/app/src/main/res/raw/style.css @@ -11,6 +11,29 @@ body { min-width: auto !important; } +#ik-quotes, #ik-quotes > * { + max-width: 100% !important; + box-sizing: border-box !important; + } + + #ik-quotes table { + width: auto !important; + max-width: 100% !important; + } + + #quote-toggle-btn { + background: none; + border: none; + cursor: pointer; + padding: 8px 12px; + display: flex; + align-items: center; + gap: 8px; + color: #666; + font-size: 13px; + font-weight: 500; + } + blockquote { padding: 0.2em 1.2em !important; margin: 0 !important; diff --git a/app/src/main/res/raw/toggle_quote_visibility_script.js b/app/src/main/res/raw/toggle_quote_visibility_script.js index 11acb23a738..03cb75afe41 100644 --- a/app/src/main/res/raw/toggle_quote_visibility_script.js +++ b/app/src/main/res/raw/toggle_quote_visibility_script.js @@ -1,33 +1,17 @@ (function() { function changeQuoteVisibility() { - // Hide quotes initially - const quoteElements = document.querySelectorAll('[id="ik-quotes"]'); - quoteElements.forEach(el => { - el.style.display = 'none'; - }); - - // Attach click handler to button const button = document.getElementById('quote-toggle-btn'); + if (!button) return; - if (button) { - button.onclick = function(e) { - e.preventDefault(); - const quoteElements = document.querySelectorAll('[id="ik-quotes"]'); - quoteElements.forEach(el => { - el.style.display = 'block'; - }); - button.style.display = 'none'; - }; - -// button.addEventListener('click', function(e) { -// e.preventDefault(); -// const quoteElements = document.querySelectorAll('[id="ik-quotes"]'); -// quoteElements.forEach(el => { -// el.style.display = 'block'; -// }); -// button.style.display = 'none'; -// }); - } + // Attach click handler to button + button.onclick = function(e) { + e.preventDefault(); + const quoteElements = document.querySelectorAll('[id="ik-quotes"]'); + quoteElements.forEach(el => { + el.style.display = 'block'; + }); + button.style.display = 'none'; + }; } changeQuoteVisibility(); From 73019fa91dd6924b2edf760289d962f8c197e229 Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Mon, 16 Feb 2026 08:56:31 +0100 Subject: [PATCH 06/94] refactor: Remove logs, unnused icon and use jsoupwithlogs --- .../mail/ui/newMessage/NewMessageFragment.kt | 18 ++++--------- .../mail/ui/newMessage/NewMessageViewModel.kt | 15 +++++------ app/src/main/res/drawable/ic_more_horiz.xml | 27 ------------------- 3 files changed, 11 insertions(+), 49 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_more_horiz.xml diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index f4c376d062f..220f6a19fe8 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -22,12 +22,10 @@ package com.infomaniak.mail.ui.newMessage import android.annotation.SuppressLint import android.app.Activity import android.content.Intent -import android.content.res.Configuration import android.os.Bundle import android.text.InputFilter import android.text.Spanned import android.transition.TransitionManager -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -89,6 +87,7 @@ import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.UiFrom import com.infomaniak.mail.ui.newMessage.encryption.EncryptionMessageManager import com.infomaniak.mail.ui.newMessage.encryption.EncryptionViewModel import com.infomaniak.mail.utils.AccountUtils +import com.infomaniak.mail.utils.JsoupParserUtil import com.infomaniak.mail.utils.MessageBodyUtils import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SignatureUtils @@ -116,7 +115,6 @@ import io.sentry.Sentry import io.sentry.SentryLevel import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import org.jsoup.Jsoup import org.jsoup.nodes.Document import splitties.experimental.ExperimentalSplittiesApi import java.util.Date @@ -344,11 +342,6 @@ class NewMessageFragment : Fragment() { ) } - override fun onConfigurationChanged(newConfig: Configuration) { - //TODO: CHECK IF WE SHOULD ADD A RELOD HERE - super.onConfigurationChanged(newConfig) - } - override fun onDestroyView() { // This block of code is needed in order to keep and reload the content of the editor across configuration changes. binding.editorWebView.exportHtml { html -> @@ -456,7 +449,7 @@ class NewMessageFragment : Fragment() { private fun removePlaceholder() = with(binding.editorWebView) { exportHtml { html -> - val doc: Document = Jsoup.parseBodyFragment(html).apply { + val doc: Document = JsoupParserUtil.jsoupParseBodyFragmentWithLog(html).apply { getElementsByClass("placeholder").first()?.text("")?.removeClass("placeholder") } @@ -543,8 +536,7 @@ class NewMessageFragment : Fragment() { trackNewMessageEvent(MatomoName.SwitchIdentity) binding.editorWebView.exportHtml { html -> - Log.d("HTML", html) - val body = Jsoup.parse(html).body() + val body = JsoupParserUtil.jsoupParseWithLog(html).body() val oldSignature = newMessageViewModel.fromLiveData.value?.signature?.content val bodyHtml = if (!oldSignature.isNullOrEmpty()) { @@ -572,13 +564,13 @@ class NewMessageFragment : Fragment() { private fun addInsideBody(bodyHtml: String, element: String): String { if (element.isEmpty()) return bodyHtml - val doc = Jsoup.parseBodyFragment(bodyHtml) + val doc = JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml) doc.body().append(element) return doc.body().html() } private fun removeSignature(html: String): String { - val doc: Document = Jsoup.parseBodyFragment(html).apply { + val doc: Document = JsoupParserUtil.jsoupParseBodyFragmentWithLog(html).apply { getElementById(MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_ID)?.remove() } 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 77d2692882f..bcb291d3483 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 @@ -84,6 +84,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 @@ -132,7 +133,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.invoke import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.jsoup.Jsoup import org.jsoup.nodes.Document import splitties.experimental.ExperimentalSplittiesApi import java.util.Date @@ -560,17 +560,14 @@ class NewMessageViewModel @Inject constructor( finalBodyContent += initialSignature } - Log.d("INITIAL QUOTE", initialQuote.toString()) if (!initialQuote.isNullOrEmpty()) { - if (!bodyHasQuotes(initialQuote.toString())) { - finalBodyContent += MessageBodyUtils.encapsulateQuotesWithInfomaniakClass(initialQuote.toString()) + finalBodyContent += if (!bodyHasQuotes(initialQuote.toString())) { + MessageBodyUtils.encapsulateQuotesWithInfomaniakClass(initialQuote.toString()) } else { - finalBodyContent += initialQuote + initialQuote } } - Log.d("BODY", finalBodyContent) - editorBodyInitializer.postValue( BodyContentPayload( content = finalBodyContent, @@ -587,12 +584,12 @@ class NewMessageViewModel @Inject constructor( } fun bodyHasPlaceholder(bodyHtml: String): Boolean { - return Jsoup.parseBodyFragment(bodyHtml).getElementsByClass("placeholder").first() != null + return JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml).getElementsByClass("placeholder").first() != null } fun bodyHasQuotes(bodyHtml: String): Boolean { Log.d("SIGNATURE HTML", bodyHtml) - return Jsoup.parseBodyFragment(bodyHtml).getElementById(INFOMANIAK_QUOTES_HTML_ID) != null + return JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml).getElementById(INFOMANIAK_QUOTES_HTML_ID) != null } private suspend fun getLocalOrRemoteDraft(localUuid: String?): Draft? { diff --git a/app/src/main/res/drawable/ic_more_horiz.xml b/app/src/main/res/drawable/ic_more_horiz.xml deleted file mode 100644 index c4d35ea4975..00000000000 --- a/app/src/main/res/drawable/ic_more_horiz.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - From 6e2580b10f84e732db926b9df669eef8e1d6b278 Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Mon, 16 Feb 2026 09:14:47 +0100 Subject: [PATCH 07/94] fix: Add signature before quotes if they exist --- .../mail/ui/newMessage/NewMessageFragment.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 220f6a19fe8..2e45328eab7 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -554,18 +554,23 @@ class NewMessageFragment : Fragment() { if (signature.isDummy) "" else signatureUtils.encapsulateSignatureContentWithInfomaniakClass(newSignatureHtml) // Combine: New Body + New Signature - val finalHtml = addInsideBody(bodyHtml, wrappedNewSignature) + val finalHtml = addSignatureInsideBody(bodyHtml, wrappedNewSignature) // Update the Editor newMessageViewModel.editorBodyInitializer.postValue(BodyContentPayload(finalHtml, BodyContentType.HTML_SANITIZED)) } } - private fun addInsideBody(bodyHtml: String, element: String): String { + private fun addSignatureInsideBody(bodyHtml: String, element: String): String { if (element.isEmpty()) return bodyHtml val doc = JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml) - doc.body().append(element) + val quotesDiv = doc.body().getElementById(MessageBodyUtils.INFOMANIAK_QUOTES_HTML_ID) + if (quotesDiv != null) { + quotesDiv.before(element) + } else { + doc.body().append(element) + } return doc.body().html() } From 75c23ac6e1d99fe3b3106e49f87e0ccc9b9734de Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Mon, 16 Feb 2026 13:41:26 +0100 Subject: [PATCH 08/94] feat: Show images on forward --- .../ReplyForwardFooterManager.kt | 18 +++++++--- .../mail/ui/newMessage/NewMessageFragment.kt | 36 +++---------------- .../mail/ui/newMessage/NewMessageViewModel.kt | 18 ++++++++++ .../mail/utils/extensions/AttachmentExt.kt | 35 ++++++++++++++++++ 4 files changed, 70 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt index 95807dc5c79..6463cdb65d9 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt @@ -31,6 +31,7 @@ import com.infomaniak.mail.utils.MessageBodyUtils import com.infomaniak.mail.utils.SharedUtils import com.infomaniak.mail.utils.Utils import com.infomaniak.mail.utils.date.MailDateFormatUtils.formatForHeader +import com.infomaniak.mail.utils.extensions.AttachmentExt.getScaledLocalBase64 import com.infomaniak.mail.utils.extensions.toDate import org.jsoup.nodes.Document import org.jsoup.nodes.Element @@ -46,14 +47,21 @@ class ReplyForwardFooterManager @Inject constructor(private val appContext: Cont document.processCids( attachments = message.attachments, associateDataToCid = { oldAttachment -> - val newAttachment = attachmentsToForward.find { it.originalContentId == oldAttachment.contentId } - newAttachment?.contentId + attachmentsToForward.find { + it.originalContentId == oldAttachment.contentId || it.name == oldAttachment.name + } }, - onCidImageFound = { newContentId, imageElement -> - imageElement.attr(SRC_ATTRIBUTE, "${CID_PROTOCOL}$newContentId") + onCidImageFound = { attachment, imageElement -> + // get the image in base64, since the richHtmlEditorWebView doesn't work correctly with Cids. + val base64 = attachment.getScaledLocalBase64(appContext) + if (base64 != null) { + imageElement.attr(SRC_ATTRIBUTE, "data:${attachment.mimeType};base64,$base64") + imageElement.attr("style", "max-width: 100%; height: auto;") + } else { + imageElement.attr(SRC_ATTRIBUTE, "${CID_PROTOCOL}${attachment.contentId}") + } }, ) - document.outerHtml() } ?: "" diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 2e45328eab7..e87d044f61e 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -88,7 +88,6 @@ import com.infomaniak.mail.ui.newMessage.encryption.EncryptionMessageManager import com.infomaniak.mail.ui.newMessage.encryption.EncryptionViewModel import com.infomaniak.mail.utils.AccountUtils import com.infomaniak.mail.utils.JsoupParserUtil -import com.infomaniak.mail.utils.MessageBodyUtils import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SignatureUtils import com.infomaniak.mail.utils.UiUtils.PRIMARY_COLOR_CODE @@ -433,9 +432,7 @@ class NewMessageFragment : Fragment() { } fun editorHasPlaceholder() = with(binding.editorWebView) { - exportHtml { html -> - hasPlaceholder = newMessageViewModel.bodyHasPlaceholder(html) - } + exportHtml { html -> hasPlaceholder = newMessageViewModel.bodyHasPlaceholder(html) } } private fun setEditorStyle() = with(binding.editorWebView) { @@ -452,12 +449,8 @@ class NewMessageFragment : Fragment() { val doc: Document = JsoupParserUtil.jsoupParseBodyFragmentWithLog(html).apply { getElementsByClass("placeholder").first()?.text("")?.removeClass("placeholder") } - newMessageViewModel.editorBodyInitializer.postValue( - BodyContentPayload( - doc.html(), - BodyContentType.HTML_SANITIZED - ) + BodyContentPayload(doc.html(), BodyContentType.HTML_SANITIZED) ) } } @@ -540,7 +533,7 @@ class NewMessageFragment : Fragment() { val oldSignature = newMessageViewModel.fromLiveData.value?.signature?.content val bodyHtml = if (!oldSignature.isNullOrEmpty()) { - removeSignature(body.html()) + newMessageViewModel.removeSignature(body.html()) } else { body.html() } @@ -554,34 +547,13 @@ class NewMessageFragment : Fragment() { if (signature.isDummy) "" else signatureUtils.encapsulateSignatureContentWithInfomaniakClass(newSignatureHtml) // Combine: New Body + New Signature - val finalHtml = addSignatureInsideBody(bodyHtml, wrappedNewSignature) + val finalHtml = newMessageViewModel.addSignatureInsideBody(bodyHtml, wrappedNewSignature) // Update the Editor newMessageViewModel.editorBodyInitializer.postValue(BodyContentPayload(finalHtml, BodyContentType.HTML_SANITIZED)) } } - private fun addSignatureInsideBody(bodyHtml: String, element: String): String { - if (element.isEmpty()) return bodyHtml - - val doc = JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml) - val quotesDiv = doc.body().getElementById(MessageBodyUtils.INFOMANIAK_QUOTES_HTML_ID) - if (quotesDiv != null) { - quotesDiv.before(element) - } else { - doc.body().append(element) - } - return doc.body().html() - } - - private fun removeSignature(html: String): String { - val doc: Document = JsoupParserUtil.jsoupParseBodyFragmentWithLog(html).apply { - getElementById(MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_ID)?.remove() - } - - return doc.html() - } - private fun updateSelectedSignatureInFromField(signature: Signature) { val defaultFormat = if (signature.senderName.isBlank()) { 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 bcb291d3483..bd9c397055e 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 @@ -476,6 +476,24 @@ class NewMessageViewModel @Inject constructor( return BodyData(BodyContentPayload(body, BodyContentType.HTML_UNSANITIZED), signature, quote) } + fun addSignatureInsideBody(bodyHtml: String, element: String): String { + if (element.isEmpty()) return bodyHtml + + return JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml).apply { + // if the mail has quotes, add the signature before the quotes. + val body = body() + body.getElementById(INFOMANIAK_QUOTES_HTML_ID)?.before(element) ?: body.append(element) + }.body().html() + } + + fun removeSignature(html: String): String { + val doc: Document = JsoupParserUtil.jsoupParseBodyFragmentWithLog(html).apply { + getElementById(MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_ID)?.remove() + } + + return doc.html() + } + private suspend fun populateWithExternalMailDataIfNeeded(draft: Draft, intent: Intent) { when (intent.action) { Intent.ACTION_SEND -> handleSingleSendIntent(draft, intent) diff --git a/app/src/main/java/com/infomaniak/mail/utils/extensions/AttachmentExt.kt b/app/src/main/java/com/infomaniak/mail/utils/extensions/AttachmentExt.kt index ae1198d532f..8b9f4ca3bb6 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/extensions/AttachmentExt.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/extensions/AttachmentExt.kt @@ -20,8 +20,11 @@ package com.infomaniak.mail.utils.extensions import android.content.ComponentName import android.content.Context import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.os.Bundle import android.provider.MediaStore.Files.FileColumns +import android.util.Base64 import androidx.core.content.FileProvider import com.infomaniak.core.common.extensions.goToAppStore import com.infomaniak.core.legacy.utils.hasSupportedApplications @@ -45,6 +48,8 @@ import com.infomaniak.mail.utils.extensions.AttachmentExt.AttachmentIntentType.S import io.realm.kotlin.Realm import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.withContext +import java.io.ByteArrayOutputStream +import kotlin.math.roundToInt import com.infomaniak.core.legacy.R as RCore object AttachmentExt { @@ -113,6 +118,36 @@ object AttachmentExt { } } + fun Attachment.getScaledLocalBase64(context: Context, maxWidth: Int = 1024): String? { + val file = getUploadLocalFile() ?: getCacheFile(context) + if (!file.exists()) return null + + return try { + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(file.absolutePath, options) + + var inSampleSize = 1 + if (options.outWidth > maxWidth) { + inSampleSize = (options.outWidth.toFloat() / maxWidth.toFloat()).roundToInt() + } + + val rescaledOptions = BitmapFactory.Options().apply { + this.inSampleSize = inSampleSize + } + + val bitmap = BitmapFactory.decodeFile(file.absolutePath, rescaledOptions) ?: return null + + val outputStream = ByteArrayOutputStream() + // Compress as JPEG to 8 0% quality to reduce size + bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) + val byteArray = outputStream.toByteArray() + + Base64.encodeToString(byteArray, Base64.NO_WRAP) + } catch (e: Exception) { + null + } + } + fun Attachment.createDownloadDialogNavArgs(intentType: AttachmentIntentType): Bundle { return DownloadAttachmentProgressDialogArgs( attachmentLocalUuid = localUuid, From 7385a7902f6f1f2dc1f22b63774545efe8426d6a Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Mon, 16 Feb 2026 14:16:38 +0100 Subject: [PATCH 09/94] fix: Use images cids and remove loadWithOverviewMode on new message --- .../ReplyForwardFooterManager.kt | 17 ++++------------- .../mail/ui/newMessage/NewMessageFragment.kt | 19 ++++++++++++++++++- .../com/infomaniak/mail/utils/WebViewUtils.kt | 4 +--- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt index 6463cdb65d9..9f29f544cf9 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt @@ -31,7 +31,6 @@ import com.infomaniak.mail.utils.MessageBodyUtils import com.infomaniak.mail.utils.SharedUtils import com.infomaniak.mail.utils.Utils import com.infomaniak.mail.utils.date.MailDateFormatUtils.formatForHeader -import com.infomaniak.mail.utils.extensions.AttachmentExt.getScaledLocalBase64 import com.infomaniak.mail.utils.extensions.toDate import org.jsoup.nodes.Document import org.jsoup.nodes.Element @@ -47,19 +46,11 @@ class ReplyForwardFooterManager @Inject constructor(private val appContext: Cont document.processCids( attachments = message.attachments, associateDataToCid = { oldAttachment -> - attachmentsToForward.find { - it.originalContentId == oldAttachment.contentId || it.name == oldAttachment.name - } + val newAttachment = attachmentsToForward.find { it.originalContentId == oldAttachment.contentId } + newAttachment?.contentId }, - onCidImageFound = { attachment, imageElement -> - // get the image in base64, since the richHtmlEditorWebView doesn't work correctly with Cids. - val base64 = attachment.getScaledLocalBase64(appContext) - if (base64 != null) { - imageElement.attr(SRC_ATTRIBUTE, "data:${attachment.mimeType};base64,$base64") - imageElement.attr("style", "max-width: 100%; height: auto;") - } else { - imageElement.attr(SRC_ATTRIBUTE, "${CID_PROTOCOL}${attachment.contentId}") - } + onCidImageFound = { newContentId, imageElement -> + imageElement.attr(SRC_ATTRIBUTE, "${CID_PROTOCOL}$newContentId") }, ) document.outerHtml() diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index e87d044f61e..508e73482f5 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -67,6 +67,7 @@ import com.infomaniak.mail.data.LocalSettings import com.infomaniak.mail.data.models.Attachment import com.infomaniak.mail.data.models.AttachmentDisposition import com.infomaniak.mail.data.models.FeatureFlag +import com.infomaniak.mail.data.models.draft.Draft import com.infomaniak.mail.data.models.draft.Draft.DraftAction import com.infomaniak.mail.data.models.draft.Draft.DraftMode import com.infomaniak.mail.data.models.mailbox.Mailbox @@ -91,6 +92,7 @@ import com.infomaniak.mail.utils.JsoupParserUtil import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SignatureUtils import com.infomaniak.mail.utils.UiUtils.PRIMARY_COLOR_CODE +import com.infomaniak.mail.utils.WebViewUtils.Companion.setupNewMessageWebViewSettings import com.infomaniak.mail.utils.extensions.AttachmentExt import com.infomaniak.mail.utils.extensions.AttachmentExt.openAttachment import com.infomaniak.mail.utils.extensions.applySideAndBottomSystemInsets @@ -101,6 +103,7 @@ import com.infomaniak.mail.utils.extensions.changeToolbarColorOnScroll import com.infomaniak.mail.utils.extensions.enableAlgorithmicDarkening import com.infomaniak.mail.utils.extensions.getAttributeColor import com.infomaniak.mail.utils.extensions.ime +import com.infomaniak.mail.utils.extensions.initWebViewClientAndBridge import com.infomaniak.mail.utils.extensions.loadCss import com.infomaniak.mail.utils.extensions.navigateToDownloadProgressDialog import com.infomaniak.mail.utils.extensions.readRawResource @@ -494,6 +497,19 @@ class NewMessageFragment : Fragment() { } } + private fun configureUiWithDraftData(draft: Draft) = with(binding) { + editorWebView.apply { + settings.setupNewMessageWebViewSettings() + val alwaysShowExternalContent = localSettings.externalContent == LocalSettings.ExternalContent.ALWAYS + initWebViewClientAndBridge( + attachments = draft.attachments, + messageUid = "QUOTE-${draft.messageUid}", + shouldLoadDistantResources = alwaysShowExternalContent || newMessageViewModel.shouldLoadDistantResources(), + navigateToNewMessageActivity = null, + ) + } + } + private fun setupFromField(signatures: List) = with(binding) { signatureAdapter.setList(signatures) @@ -572,7 +588,8 @@ class NewMessageFragment : Fragment() { } private fun observeInitResult() { - newMessageViewModel.initResult.observe(viewLifecycleOwner) { (_, signatures) -> + newMessageViewModel.initResult.observe(viewLifecycleOwner) { (draft, signatures) -> + configureUiWithDraftData(draft) setupFromField(signatures) editorManager.setupEditorFormatActions() editorManager.setupEditorFormatActionsToggle() diff --git a/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt index de1cb26180e..0b2c620c814 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt @@ -138,15 +138,13 @@ class WebViewUtils(context: Context) { @SuppressLint("SetJavaScriptEnabled") javaScriptEnabled = true - loadWithOverviewMode = true - useWideViewPort = true - cacheMode = LOAD_CACHE_ELSE_NETWORK } fun WebSettings.setupThreadWebViewSettings() { setupCommonWebViewSettings() + loadWithOverviewMode = true setSupportZoom(true) builtInZoomControls = true displayZoomControls = false From 4d59bb03f2a486b1b09caed0aaa7317016c348f3 Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Mon, 16 Feb 2026 14:29:34 +0100 Subject: [PATCH 10/94] refactor: Remove unnecesary log --- .../com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt | 2 -- 1 file changed, 2 deletions(-) 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 bd9c397055e..16259a7e832 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 @@ -24,7 +24,6 @@ import android.content.ClipDescription import android.content.Intent import android.net.Uri import android.os.Parcelable -import android.util.Log import androidx.core.app.NotificationManagerCompat import androidx.core.net.MailTo import androidx.core.net.toUri @@ -606,7 +605,6 @@ class NewMessageViewModel @Inject constructor( } fun bodyHasQuotes(bodyHtml: String): Boolean { - Log.d("SIGNATURE HTML", bodyHtml) return JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml).getElementById(INFOMANIAK_QUOTES_HTML_ID) != null } From d5957bf75a86782929c39cfa6af1e1fc009a44c0 Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Mon, 16 Feb 2026 14:33:19 +0100 Subject: [PATCH 11/94] refactor: Remove unused function --- .../mail/utils/extensions/AttachmentExt.kt | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/utils/extensions/AttachmentExt.kt b/app/src/main/java/com/infomaniak/mail/utils/extensions/AttachmentExt.kt index 8b9f4ca3bb6..ae1198d532f 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/extensions/AttachmentExt.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/extensions/AttachmentExt.kt @@ -20,11 +20,8 @@ package com.infomaniak.mail.utils.extensions import android.content.ComponentName import android.content.Context import android.content.Intent -import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.os.Bundle import android.provider.MediaStore.Files.FileColumns -import android.util.Base64 import androidx.core.content.FileProvider import com.infomaniak.core.common.extensions.goToAppStore import com.infomaniak.core.legacy.utils.hasSupportedApplications @@ -48,8 +45,6 @@ import com.infomaniak.mail.utils.extensions.AttachmentExt.AttachmentIntentType.S import io.realm.kotlin.Realm import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.withContext -import java.io.ByteArrayOutputStream -import kotlin.math.roundToInt import com.infomaniak.core.legacy.R as RCore object AttachmentExt { @@ -118,36 +113,6 @@ object AttachmentExt { } } - fun Attachment.getScaledLocalBase64(context: Context, maxWidth: Int = 1024): String? { - val file = getUploadLocalFile() ?: getCacheFile(context) - if (!file.exists()) return null - - return try { - val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeFile(file.absolutePath, options) - - var inSampleSize = 1 - if (options.outWidth > maxWidth) { - inSampleSize = (options.outWidth.toFloat() / maxWidth.toFloat()).roundToInt() - } - - val rescaledOptions = BitmapFactory.Options().apply { - this.inSampleSize = inSampleSize - } - - val bitmap = BitmapFactory.decodeFile(file.absolutePath, rescaledOptions) ?: return null - - val outputStream = ByteArrayOutputStream() - // Compress as JPEG to 8 0% quality to reduce size - bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) - val byteArray = outputStream.toByteArray() - - Base64.encodeToString(byteArray, Base64.NO_WRAP) - } catch (e: Exception) { - null - } - } - fun Attachment.createDownloadDialogNavArgs(intentType: AttachmentIntentType): Bundle { return DownloadAttachmentProgressDialogArgs( attachmentLocalUuid = localUuid, From 7143acd588239cd700f265301eaa437969e92b1a Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Mon, 16 Feb 2026 14:34:19 +0100 Subject: [PATCH 12/94] refactor: Fix identation in style css --- app/src/main/res/raw/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/raw/style.css b/app/src/main/res/raw/style.css index 310584792c3..b1c3c4539e4 100644 --- a/app/src/main/res/raw/style.css +++ b/app/src/main/res/raw/style.css @@ -32,7 +32,7 @@ body { color: #666; font-size: 13px; font-weight: 500; - } + } blockquote { padding: 0.2em 1.2em !important; From 1b1bb1e0e929f6a96332d8665cdefd824056cdef Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Mon, 16 Feb 2026 15:03:45 +0100 Subject: [PATCH 13/94] refactor: Fix sonar issues --- .../java/com/infomaniak/mail/utils/MessageBodyUtils.kt | 7 ++++++- .../main/java/com/infomaniak/mail/utils/SignatureUtils.kt | 6 +++++- app/src/main/res/raw/editor_style.css | 1 - 3 files changed, 11 insertions(+), 3 deletions(-) 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 6866584fe16..fef33137ebc 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt @@ -67,7 +67,12 @@ object MessageBodyUtils { ) fun encapsulateQuotesWithInfomaniakClass(quotes: String): String { - return """
${toggleButton()}
""" + return """ +
+ + ${toggleButton()} +
+ """.trimMargin() } fun toggleButton(): String { diff --git a/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt index 99b89f08d85..5029f978b17 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt @@ -33,7 +33,11 @@ class SignatureUtils @Inject constructor(appContext: Context) { fun encapsulateSignatureContentWithInfomaniakClass(signatureContent: String): String { val verticalMarginsCss = signatureMargins val verticalMarginAttributes = extractAttributesFromMarginCss(verticalMarginsCss) - return """
$signatureContent
""" + return """ +
+ $signatureContent +
+ """.trimIndent() } private fun extractAttributesFromMarginCss(verticalMarginsCss: String): String { diff --git a/app/src/main/res/raw/editor_style.css b/app/src/main/res/raw/editor_style.css index 53ddda9d22c..ffda7a77f13 100644 --- a/app/src/main/res/raw/editor_style.css +++ b/app/src/main/res/raw/editor_style.css @@ -5,7 +5,6 @@ html { body { margin-top: 0px; margin-bottom: 1rem; - font-family: 'Helvetica'; } p { From cbe18628aa9994f53626bc50731391f877478980 Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Wed, 25 Feb 2026 13:16:25 +0100 Subject: [PATCH 14/94] feat: Add textview placeholder and change signature with js --- .../mail/ui/newMessage/BodyContentPayload.kt | 8 +- .../mail/ui/newMessage/NewMessageFragment.kt | 93 ++++++++----------- .../mail/ui/newMessage/NewMessageViewModel.kt | 32 ++----- .../infomaniak/mail/utils/MessageBodyUtils.kt | 8 +- .../infomaniak/mail/utils/SignatureUtils.kt | 3 +- .../main/res/layout/fragment_new_message.xml | 17 ++++ app/src/main/res/raw/editor_style.css | 4 - .../res/raw/toggle_quote_visibility_script.js | 12 ++- 8 files changed, 84 insertions(+), 93 deletions(-) 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 98793d17578..f445b35b1e1 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,8 +17,6 @@ */ package com.infomaniak.mail.ui.newMessage -import com.infomaniak.mail.utils.MessageBodyUtils.INFOMANIAK_BODY_HTML_ID - /** * @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,10 +24,10 @@ import com.infomaniak.mail.utils.MessageBodyUtils.INFOMANIAK_BODY_HTML_ID data class BodyContentPayload(val content: String, val type: BodyContentType) { companion object { - fun emptyBody(placeHolderText: String) = + fun emptyBody() = BodyContentPayload( - content = "

$placeHolderText


", - type = BodyContentType.HTML_SANITIZED + content = "
", + type = BodyContentType.TEXT_PLAIN_WITHOUT_HTML ) } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 508e73482f5..8826313944e 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -67,7 +67,6 @@ import com.infomaniak.mail.data.LocalSettings import com.infomaniak.mail.data.models.Attachment import com.infomaniak.mail.data.models.AttachmentDisposition import com.infomaniak.mail.data.models.FeatureFlag -import com.infomaniak.mail.data.models.draft.Draft import com.infomaniak.mail.data.models.draft.Draft.DraftAction import com.infomaniak.mail.data.models.draft.Draft.DraftMode import com.infomaniak.mail.data.models.mailbox.Mailbox @@ -88,7 +87,7 @@ import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.UiFrom import com.infomaniak.mail.ui.newMessage.encryption.EncryptionMessageManager import com.infomaniak.mail.ui.newMessage.encryption.EncryptionViewModel import com.infomaniak.mail.utils.AccountUtils -import com.infomaniak.mail.utils.JsoupParserUtil +import com.infomaniak.mail.utils.MessageBodyUtils import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SignatureUtils import com.infomaniak.mail.utils.UiUtils.PRIMARY_COLOR_CODE @@ -103,7 +102,6 @@ import com.infomaniak.mail.utils.extensions.changeToolbarColorOnScroll import com.infomaniak.mail.utils.extensions.enableAlgorithmicDarkening import com.infomaniak.mail.utils.extensions.getAttributeColor import com.infomaniak.mail.utils.extensions.ime -import com.infomaniak.mail.utils.extensions.initWebViewClientAndBridge import com.infomaniak.mail.utils.extensions.loadCss import com.infomaniak.mail.utils.extensions.navigateToDownloadProgressDialog import com.infomaniak.mail.utils.extensions.readRawResource @@ -117,7 +115,6 @@ import io.sentry.Sentry import io.sentry.SentryLevel import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import org.jsoup.nodes.Document import splitties.experimental.ExperimentalSplittiesApi import java.util.Date import javax.inject.Inject @@ -434,10 +431,6 @@ class NewMessageFragment : Fragment() { handleFocusChanges() } - fun editorHasPlaceholder() = with(binding.editorWebView) { - exportHtml { html -> hasPlaceholder = newMessageViewModel.bodyHasPlaceholder(html) } - } - private fun setEditorStyle() = with(binding.editorWebView) { enableAlgorithmicDarkening(isEnabled = true) if (context.isNightModeEnabled()) addCss(context.loadCss(R.raw.custom_dark_mode)) @@ -447,15 +440,9 @@ class NewMessageFragment : Fragment() { addCss(context.loadCss(R.raw.editor_style, customColors)) } - private fun removePlaceholder() = with(binding.editorWebView) { - exportHtml { html -> - val doc: Document = JsoupParserUtil.jsoupParseBodyFragmentWithLog(html).apply { - getElementsByClass("placeholder").first()?.text("")?.removeClass("placeholder") - } - newMessageViewModel.editorBodyInitializer.postValue( - BodyContentPayload(doc.html(), BodyContentType.HTML_SANITIZED) - ) - } + private fun removePlaceholder() { + binding.newMessagePlaceholder.visibility = View.GONE + hasPlaceholder = false } private fun handleFocusChanges() { @@ -476,6 +463,7 @@ class NewMessageFragment : Fragment() { initDraftAndViewModel(intent = requireActivity().intent).observe(viewLifecycleOwner) { draft -> if (draft != null) { val isBodyEmpty = newMessageViewModel.bodyHasPlaceholder(draft.body) + binding.newMessagePlaceholder.isVisible = isBodyEmpty showKeyboardInCorrectView(isToFieldEmpty = draft.to.isEmpty(), isBodyEmpty = isBodyEmpty) binding.subjectTextField.setText(draft.subject) } else { @@ -497,18 +485,7 @@ class NewMessageFragment : Fragment() { } } - private fun configureUiWithDraftData(draft: Draft) = with(binding) { - editorWebView.apply { - settings.setupNewMessageWebViewSettings() - val alwaysShowExternalContent = localSettings.externalContent == LocalSettings.ExternalContent.ALWAYS - initWebViewClientAndBridge( - attachments = draft.attachments, - messageUid = "QUOTE-${draft.messageUid}", - shouldLoadDistantResources = alwaysShowExternalContent || newMessageViewModel.shouldLoadDistantResources(), - navigateToNewMessageActivity = null, - ) - } - } + private fun configureUiWithDraftData() = binding.editorWebView.settings.setupNewMessageWebViewSettings() private fun setupFromField(signatures: List) = with(binding) { @@ -544,29 +521,42 @@ class NewMessageFragment : Fragment() { private fun onSignatureClicked(signature: Signature) { trackNewMessageEvent(MatomoName.SwitchIdentity) - binding.editorWebView.exportHtml { html -> - val body = JsoupParserUtil.jsoupParseWithLog(html).body() - val oldSignature = newMessageViewModel.fromLiveData.value?.signature?.content - - val bodyHtml = if (!oldSignature.isNullOrEmpty()) { - newMessageViewModel.removeSignature(body.html()) - } else { - body.html() + // Get the New Signature HTML + val newSignatureHtml = signature.content + val wrappedNewSignature = + if (signature.isDummy) "" else signatureUtils.encapsulateSignatureContentWithInfomaniakClass(newSignatureHtml) + + val escapedSignature = wrappedNewSignature + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + + val replaceSignatureScript = """ + (function() { + var sigElement = document.querySelector('.${MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_CLASS_NAME}'); + var newSigHtml = "$escapedSignature"; + if (sigElement) { + if (newSigHtml === "") { + sigElement.remove(); + } else { + sigElement.outerHTML = newSigHtml; + } + } else if (newSigHtml !== "") { + var quotes = document.querySelector('.${MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME}'); + if (quotes) { + quotes.insertAdjacentHTML('beforebegin', newSigHtml); + } else { + document.body.insertAdjacentHTML('beforeend', newSigHtml); + } } + })() + """.trimIndent() - newMessageViewModel.fromLiveData.value = UiFrom(signature) + binding.editorWebView.evaluateJavascript(replaceSignatureScript) { + newMessageViewModel.fromLiveData.value = + UiFrom(signature) addressListPopupWindow?.dismiss() - - // Get the New Signature HTML - val newSignatureHtml = signature.content - val wrappedNewSignature = - if (signature.isDummy) "" else signatureUtils.encapsulateSignatureContentWithInfomaniakClass(newSignatureHtml) - - // Combine: New Body + New Signature - val finalHtml = newMessageViewModel.addSignatureInsideBody(bodyHtml, wrappedNewSignature) - - // Update the Editor - newMessageViewModel.editorBodyInitializer.postValue(BodyContentPayload(finalHtml, BodyContentType.HTML_SANITIZED)) } } @@ -588,8 +578,8 @@ class NewMessageFragment : Fragment() { } private fun observeInitResult() { - newMessageViewModel.initResult.observe(viewLifecycleOwner) { (draft, signatures) -> - configureUiWithDraftData(draft) + newMessageViewModel.initResult.observe(viewLifecycleOwner) { (_, signatures) -> + configureUiWithDraftData() setupFromField(signatures) editorManager.setupEditorFormatActions() editorManager.setupEditorFormatActionsToggle() @@ -697,7 +687,6 @@ class NewMessageFragment : Fragment() { editorContentManager.setContent(binding.editorWebView, body) val script = context?.readRawResource(R.raw.toggle_quote_visibility_script) script?.let { binding.editorWebView.addScript(script) } - editorHasPlaceholder() } } 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 16259a7e832..45800b1b327 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 @@ -24,6 +24,7 @@ import android.content.ClipDescription import android.content.Intent import android.net.Uri import android.os.Parcelable +import android.util.Log import androidx.core.app.NotificationManagerCompat import androidx.core.net.MailTo import androidx.core.net.toUri @@ -87,7 +88,6 @@ 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 -import com.infomaniak.mail.utils.MessageBodyUtils.INFOMANIAK_QUOTES_HTML_ID import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SharedUtils import com.infomaniak.mail.utils.SignatureUtils @@ -162,8 +162,7 @@ class NewMessageViewModel @Inject constructor( private val ioCoroutineContext = viewModelScope.coroutineContext(ioDispatcher) //region Initial data - private var initialBody: BodyContentPayload = - BodyContentPayload.emptyBody(appContext.getString(R.string.newMessagePlaceholderTitle)) + private var initialBody: BodyContentPayload = BodyContentPayload.emptyBody() private var initialSignature: String? = null private var initialQuote: String? = null //endregion @@ -427,6 +426,7 @@ class NewMessageViewModel @Inject constructor( private fun splitSignatureAndQuoteFromBody(draft: Draft) { val remoteBody = draft.body + Log.d("DRAFT BODY", remoteBody) if (remoteBody.isEmpty()) return val (body, signature, quote) = when (draft.mimeType) { @@ -475,24 +475,6 @@ class NewMessageViewModel @Inject constructor( return BodyData(BodyContentPayload(body, BodyContentType.HTML_UNSANITIZED), signature, quote) } - fun addSignatureInsideBody(bodyHtml: String, element: String): String { - if (element.isEmpty()) return bodyHtml - - return JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml).apply { - // if the mail has quotes, add the signature before the quotes. - val body = body() - body.getElementById(INFOMANIAK_QUOTES_HTML_ID)?.before(element) ?: body.append(element) - }.body().html() - } - - fun removeSignature(html: String): String { - val doc: Document = JsoupParserUtil.jsoupParseBodyFragmentWithLog(html).apply { - getElementById(MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_ID)?.remove() - } - - return doc.html() - } - private suspend fun populateWithExternalMailDataIfNeeded(draft: Draft, intent: Intent) { when (intent.action) { Intent.ACTION_SEND -> handleSingleSendIntent(draft, intent) @@ -601,11 +583,13 @@ class NewMessageViewModel @Inject constructor( } fun bodyHasPlaceholder(bodyHtml: String): Boolean { - return JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml).getElementsByClass("placeholder").first() != null + val body = JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml).body() + return body.text() == "" } fun bodyHasQuotes(bodyHtml: String): Boolean { - return JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml).getElementById(INFOMANIAK_QUOTES_HTML_ID) != null + return JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml) + .getElementsByClass(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME) != null } private suspend fun getLocalOrRemoteDraft(localUuid: String?): Draft? { @@ -743,7 +727,7 @@ class NewMessageViewModel @Inject constructor( val bodyContent = mailToIntent?.body?.takeIf(String::isNotEmpty) ?: intent?.getStringExtra(Intent.EXTRA_TEXT) val mailToPayload = bodyContent?.let { BodyContentPayload(it, BodyContentType.TEXT_PLAIN_WITH_HTML) } - initialBody = mailToPayload ?: BodyContentPayload.emptyBody(appContext.getString(R.string.newMessagePlaceholderTitle)) + initialBody = mailToPayload ?: BodyContentPayload.emptyBody() } } 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 fef33137ebc..9cc14e3b59d 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt @@ -35,8 +35,6 @@ object MessageBodyUtils { const val INFOMANIAK_SIGNATURE_HTML_CLASS_NAME = "editorUserSignature" const val INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME = "ik_mail_quote" const val INFOMANIAK_FORWARD_QUOTE_HTML_CLASS_NAME = "forwardContentMessage" - const val INFOMANIAK_BODY_HTML_ID = "ik-body" - const val INFOMANIAK_SIGNATURE_HTML_ID = "ik-signature" const val INFOMANIAK_QUOTES_HTML_ID = "ik-quotes" private const val QUOTE_DETECTION_TIMEOUT = 1_500L @@ -68,11 +66,11 @@ object MessageBodyUtils { fun encapsulateQuotesWithInfomaniakClass(quotes: String): String { return """ -
- +
+
$quotes
${toggleButton()}
- """.trimMargin() + """.trimIndent() } fun toggleButton(): String { diff --git a/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt index 5029f978b17..65f8f0a4821 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt @@ -20,7 +20,6 @@ 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.MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_ID import com.infomaniak.mail.utils.extensions.loadCss import javax.inject.Inject import javax.inject.Singleton @@ -34,7 +33,7 @@ class SignatureUtils @Inject constructor(appContext: Context) { val verticalMarginsCss = signatureMargins val verticalMarginAttributes = extractAttributesFromMarginCss(verticalMarginsCss) return """ -
+
$signatureContent
""".trimIndent() diff --git a/app/src/main/res/layout/fragment_new_message.xml b/app/src/main/res/layout/fragment_new_message.xml index 0540ff656f0..ed47932abcf 100644 --- a/app/src/main/res/layout/fragment_new_message.xml +++ b/app/src/main/res/layout/fragment_new_message.xml @@ -302,6 +302,23 @@ app:layout_constraintTop_toBottomOf="@id/attachmentsRecyclerView" tools:layout_height="100dp" /> + + + { + el.style.display = 'none'; + }); + button.style.display = 'block'; + } + // Attach click handler to button button.onclick = function(e) { e.preventDefault(); - const quoteElements = document.querySelectorAll('[id="ik-quotes"]'); quoteElements.forEach(el => { el.style.display = 'block'; }); From 306f48950a39e892023b61bdf7ea4c3e02c4249d Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Wed, 25 Feb 2026 15:27:28 +0100 Subject: [PATCH 15/94] feat: Remove splitting of mail body if it's a draft and fix save snapshot logic --- .../mail/ui/newMessage/NewMessageViewModel.kt | 47 ++++++++++++------- .../infomaniak/mail/utils/MessageBodyUtils.kt | 8 +--- .../res/raw/toggle_quote_visibility_script.js | 2 +- 3 files changed, 31 insertions(+), 26 deletions(-) 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 45800b1b327..0116bbfcd29 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 @@ -88,6 +88,7 @@ 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 +import com.infomaniak.mail.utils.MessageBodyUtils.toggleButton import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SharedUtils import com.infomaniak.mail.utils.SignatureUtils @@ -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)) } @@ -344,7 +343,10 @@ 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 } } @@ -555,21 +557,22 @@ class NewMessageViewModel @Inject constructor( if (isNewMessage && wrappedSignature != null) { finalBodyContent += wrappedSignature - } else if (!initialSignature.isNullOrEmpty()) { - finalBodyContent += initialSignature } - if (!initialQuote.isNullOrEmpty()) { - finalBodyContent += if (!bodyHasQuotes(initialQuote.toString())) { - MessageBodyUtils.encapsulateQuotesWithInfomaniakClass(initialQuote.toString()) - } else { - initialQuote - } + if (isNewMessage && !initialQuote.isNullOrEmpty()) { + finalBodyContent += MessageBodyUtils.encapsulateQuotesWithInfomaniakClass(initialQuote.toString()) + } + + if (bodyHasQuotes(finalBodyContent)) { + finalBodyContent += toggleButton() } + val normalizedBody = normalizeHtml(finalBodyContent) + saveSnapshot(normalizedBody) + editorBodyInitializer.postValue( BodyContentPayload( - content = finalBodyContent, + content = normalizedBody, type = BodyContentType.HTML_SANITIZED ) ) @@ -589,7 +592,13 @@ class NewMessageViewModel @Inject constructor( fun bodyHasQuotes(bodyHtml: String): Boolean { return JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml) - .getElementsByClass(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME) != null + .getElementsByClass(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME).count() > 0 + } + + 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? { @@ -986,7 +995,7 @@ class NewMessageViewModel @Inject constructor( ) subject = subjectValue - body = sanitizeBodyBeforeSaving(uiBodyValue) + body = uiBodyValue /** * If we are opening for the 1st time an existing Draft created somewhere else @@ -1002,19 +1011,19 @@ 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(uiBodyValue) + copyFromRealm().saveSnapshot(normalizedBody) isNewMessage = false } } - private fun sanitizeBodyBeforeSaving(html: String): String { + private fun sanitizeBody(html: String): String { val doc = jsoupParseWithLog(html) // Remove the toggle button before saving doc.getElementById("quote-toggle-btn")?.remove() return doc.html() } - private fun Draft.updateDraftAttachmentsWithLiveData(uiAttachments: List, step: String) { /** @@ -1054,13 +1063,15 @@ class NewMessageViewModel @Inject constructor( } private fun isSnapshotTheSame(subjectValue: String?, uiBodyValue: String): Boolean { + Log.d("BODY VALUE", uiBodyValue) + Log.d("SNAPSHOT", snapshot?.uiBody.toString()) return snapshot?.let { draftSnapshot -> draftSnapshot.identityId == fromLiveData.value?.signature?.id?.toString() && draftSnapshot.to == toLiveData.valueOrEmpty().toSet() && 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() 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 9cc14e3b59d..6f43b96b2a7 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt @@ -35,7 +35,6 @@ object MessageBodyUtils { const val INFOMANIAK_SIGNATURE_HTML_CLASS_NAME = "editorUserSignature" const val INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME = "ik_mail_quote" const val INFOMANIAK_FORWARD_QUOTE_HTML_CLASS_NAME = "forwardContentMessage" - const val INFOMANIAK_QUOTES_HTML_ID = "ik-quotes" private const val QUOTE_DETECTION_TIMEOUT = 1_500L @@ -65,12 +64,7 @@ object MessageBodyUtils { ) fun encapsulateQuotesWithInfomaniakClass(quotes: String): String { - return """ -
-
$quotes
- ${toggleButton()} -
- """.trimIndent() + return """
$quotes
""".trimIndent() } fun toggleButton(): String { diff --git a/app/src/main/res/raw/toggle_quote_visibility_script.js b/app/src/main/res/raw/toggle_quote_visibility_script.js index e6e1e30b1c5..5727a9745ec 100644 --- a/app/src/main/res/raw/toggle_quote_visibility_script.js +++ b/app/src/main/res/raw/toggle_quote_visibility_script.js @@ -6,7 +6,7 @@ // Hide quotes initially and show toggle button const quoteElements = document.querySelectorAll('.ik_mail_quote'); - console.log("QUOTES ELEMENTS", quotesElements) + if (quoteElements.style != 'block') { quoteElements.forEach(el => { el.style.display = 'none'; From 86f88c9e27915258ffade4be42faaeb39443dd97 Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Thu, 26 Feb 2026 15:38:25 +0100 Subject: [PATCH 16/94] fix: Fix cid images and webclient for attachments --- .../mail/ui/newMessage/NewMessageFragment.kt | 42 +++++++++++------ .../mail/ui/newMessage/NewMessageViewModel.kt | 24 +++++++--- .../infomaniak/mail/utils/HtmlFormatter.kt | 10 ++++ .../com/infomaniak/mail/utils/WebViewUtils.kt | 11 ----- app/src/main/res/drawable/ic_more_horiz.xml | 28 +++++++++++ .../main/res/layout/fragment_new_message.xml | 10 ++++ app/src/main/res/raw/signature_margins.css | 2 +- app/src/main/res/raw/style.css | 46 ++++++++++--------- .../res/raw/toggle_quote_visibility_script.js | 14 ++++-- 9 files changed, 129 insertions(+), 58 deletions(-) create mode 100644 app/src/main/res/drawable/ic_more_horiz.xml diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 8826313944e..9881ccf01ff 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -67,6 +67,7 @@ import com.infomaniak.mail.data.LocalSettings import com.infomaniak.mail.data.models.Attachment import com.infomaniak.mail.data.models.AttachmentDisposition import com.infomaniak.mail.data.models.FeatureFlag +import com.infomaniak.mail.data.models.draft.Draft import com.infomaniak.mail.data.models.draft.Draft.DraftAction import com.infomaniak.mail.data.models.draft.Draft.DraftMode import com.infomaniak.mail.data.models.mailbox.Mailbox @@ -87,10 +88,15 @@ import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.UiFrom import com.infomaniak.mail.ui.newMessage.encryption.EncryptionMessageManager import com.infomaniak.mail.ui.newMessage.encryption.EncryptionViewModel import com.infomaniak.mail.utils.AccountUtils +import com.infomaniak.mail.utils.HtmlFormatter.Companion.getCustomDarkMode +import com.infomaniak.mail.utils.HtmlFormatter.Companion.getCustomEditorStyle +import com.infomaniak.mail.utils.HtmlFormatter.Companion.getCustomStyle +import com.infomaniak.mail.utils.HtmlFormatter.Companion.getSignatureMarginStyle +import com.infomaniak.mail.utils.HtmlFormatter.Companion.getToggleQuotesButtonVisibilityScript import com.infomaniak.mail.utils.MessageBodyUtils import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SignatureUtils -import com.infomaniak.mail.utils.UiUtils.PRIMARY_COLOR_CODE +import com.infomaniak.mail.utils.WebViewUtils import com.infomaniak.mail.utils.WebViewUtils.Companion.setupNewMessageWebViewSettings import com.infomaniak.mail.utils.extensions.AttachmentExt import com.infomaniak.mail.utils.extensions.AttachmentExt.openAttachment @@ -100,11 +106,9 @@ import com.infomaniak.mail.utils.extensions.applyWindowInsetsListener import com.infomaniak.mail.utils.extensions.bindAlertToViewLifecycle import com.infomaniak.mail.utils.extensions.changeToolbarColorOnScroll import com.infomaniak.mail.utils.extensions.enableAlgorithmicDarkening -import com.infomaniak.mail.utils.extensions.getAttributeColor import com.infomaniak.mail.utils.extensions.ime -import com.infomaniak.mail.utils.extensions.loadCss +import com.infomaniak.mail.utils.extensions.initWebViewClientAndBridge import com.infomaniak.mail.utils.extensions.navigateToDownloadProgressDialog -import com.infomaniak.mail.utils.extensions.readRawResource import com.infomaniak.mail.utils.extensions.systemBars import com.infomaniak.mail.utils.extensions.valueOrEmpty import com.infomaniak.mail.utils.openKSuiteProBottomSheet @@ -118,7 +122,6 @@ import kotlinx.coroutines.launch import splitties.experimental.ExperimentalSplittiesApi import java.util.Date import javax.inject.Inject -import androidx.appcompat.R as RAndroid @AndroidEntryPoint class NewMessageFragment : Fragment() { @@ -146,6 +149,7 @@ class NewMessageFragment : Fragment() { private val attachmentAdapter inline get() = binding.attachmentsRecyclerView.adapter as AttachmentAdapter private val newMessageActivity by lazy { requireActivity() as NewMessageActivity } + private val webViewUtils by lazy { WebViewUtils(requireContext()) } @Inject lateinit var editorContentManager: EditorContentManager @@ -183,6 +187,7 @@ class NewMessageFragment : Fragment() { @Inject lateinit var dateAndTimeScheduleDialog: SelectDateAndTimeForScheduledDraftDialog + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return FragmentNewMessageBinding.inflate(inflater, container, false).also { _binding = it }.root } @@ -433,11 +438,10 @@ class NewMessageFragment : Fragment() { private fun setEditorStyle() = with(binding.editorWebView) { enableAlgorithmicDarkening(isEnabled = true) - if (context.isNightModeEnabled()) addCss(context.loadCss(R.raw.custom_dark_mode)) - - val customColors = listOf(PRIMARY_COLOR_CODE to context.getAttributeColor(RAndroid.attr.colorPrimary)) - addCss(context.loadCss(R.raw.style, customColors)) - addCss(context.loadCss(R.raw.editor_style, customColors)) + if (context.isNightModeEnabled()) addCss(context.getCustomDarkMode()) + addCss(context.getCustomStyle()) + addCss(context.getCustomEditorStyle()) + addCss(context.getSignatureMarginStyle()) } private fun removePlaceholder() { @@ -485,7 +489,16 @@ class NewMessageFragment : Fragment() { } } - private fun configureUiWithDraftData() = binding.editorWebView.settings.setupNewMessageWebViewSettings() + private fun configureUiWithDraftData(draft: Draft) = with(binding.editorWebView) { + settings.setupNewMessageWebViewSettings() + webViewClient = + initWebViewClientAndBridge( + attachments = draft.attachments, + messageUid = "MESSAGE-" + draft.messageUid, + shouldLoadDistantResources = true, + navigateToNewMessageActivity = null + ) + } private fun setupFromField(signatures: List) = with(binding) { @@ -578,8 +591,8 @@ class NewMessageFragment : Fragment() { } private fun observeInitResult() { - newMessageViewModel.initResult.observe(viewLifecycleOwner) { (_, signatures) -> - configureUiWithDraftData() + newMessageViewModel.initResult.observe(viewLifecycleOwner) { (draft, signatures) -> + configureUiWithDraftData(draft) setupFromField(signatures) editorManager.setupEditorFormatActions() editorManager.setupEditorFormatActionsToggle() @@ -685,8 +698,7 @@ class NewMessageFragment : Fragment() { private fun observeBodyLoader() { newMessageViewModel.editorBodyInitializer.observe(viewLifecycleOwner) { body -> editorContentManager.setContent(binding.editorWebView, body) - val script = context?.readRawResource(R.raw.toggle_quote_visibility_script) - script?.let { binding.editorWebView.addScript(script) } + binding.editorWebView.addScript(requireContext().getToggleQuotesButtonVisibilityScript()) } } 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 0116bbfcd29..7cd7116a16a 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 @@ -563,13 +563,13 @@ class NewMessageViewModel @Inject constructor( finalBodyContent += MessageBodyUtils.encapsulateQuotesWithInfomaniakClass(initialQuote.toString()) } + var normalizedBody = normalizeHtml(finalBodyContent) + saveSnapshot(normalizedBody) + if (bodyHasQuotes(finalBodyContent)) { - finalBodyContent += toggleButton() + normalizedBody += toggleButton() } - val normalizedBody = normalizeHtml(finalBodyContent) - saveSnapshot(normalizedBody) - editorBodyInitializer.postValue( BodyContentPayload( content = normalizedBody, @@ -591,8 +591,11 @@ class NewMessageViewModel @Inject constructor( } fun bodyHasQuotes(bodyHtml: String): Boolean { - return JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml) + val bodyHasQuotes = JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml) .getElementsByClass(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME).count() > 0 + val toggleButtonAlreadyExists = JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml) + .getElementById("quote-toggle-btn") != null + return bodyHasQuotes && !toggleButtonAlreadyExists } private fun normalizeHtml(html: String): String { @@ -995,7 +998,7 @@ class NewMessageViewModel @Inject constructor( ) subject = subjectValue - body = uiBodyValue + body = sanitizeBody(uiBodyValue) /** * If we are opening for the 1st time an existing Draft created somewhere else @@ -1011,7 +1014,7 @@ 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) { - val normalizedBody = normalizeHtml(uiBodyValue) + val normalizedBody = normalizeHtml(body) copyFromRealm().saveSnapshot(normalizedBody) isNewMessage = false } @@ -1019,6 +1022,13 @@ class NewMessageViewModel @Inject constructor( private fun sanitizeBody(html: String): String { val doc = jsoupParseWithLog(html) + // If the user deleted the quotes text, remove the quotes div. + val bodyHasEmptyQuotes = JsoupParserUtil.jsoupParseBodyFragmentWithLog(html) + .getElementsByClass(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME).first()?.text() == "" + if (bodyHasEmptyQuotes) { + doc.getElementsByClass(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME).forEach { it.remove() } + } + // Remove the toggle button before saving doc.getElementById("quote-toggle-btn")?.remove() return doc.html() diff --git a/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt b/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt index 85e7c24cf22..b1f910e18f5 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt @@ -219,6 +219,12 @@ 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) @@ -228,6 +234,10 @@ class HtmlFormatter(private val html: String) { listOf("MESSAGE_SELECTOR" to "#$KMAIL_MESSAGE_ID") ) + fun Context.getToggleQuotesButtonVisibilityScript(): String = loadScript( + R.raw.toggle_quote_visibility_script + ) + fun Context.getFixStyleScript(): String { return loadScript(R.raw.fix_email_style) } diff --git a/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt index 0b2c620c814..6d2fcfb7716 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt @@ -33,7 +33,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 import kotlin.math.abs @@ -43,7 +42,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() } @@ -68,15 +66,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) diff --git a/app/src/main/res/drawable/ic_more_horiz.xml b/app/src/main/res/drawable/ic_more_horiz.xml new file mode 100644 index 00000000000..602d0daa2d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_horiz.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_new_message.xml b/app/src/main/res/layout/fragment_new_message.xml index ed47932abcf..4ae09d4a17a 100644 --- a/app/src/main/res/layout/fragment_new_message.xml +++ b/app/src/main/res/layout/fragment_new_message.xml @@ -318,6 +318,16 @@ app:layout_constraintTop_toTopOf="@id/editorWebView" tools:visibility="visible" /> + * { - max-width: 100% !important; - box-sizing: border-box !important; - } - - #ik-quotes table { - width: auto !important; - max-width: 100% !important; - } - - #quote-toggle-btn { - background: none; - border: none; - cursor: pointer; - padding: 8px 12px; - display: flex; - align-items: center; - gap: 8px; - color: #666; - font-size: 13px; - font-weight: 500; - } + max-width: 100% !important; + box-sizing: border-box !important; +} + +#ik-quotes table { + width: auto !important; + max-width: 100% !important; +} + +#quote-toggle-btn { + background: none; + border: none; + cursor: pointer; + padding: 8px 12px; + display: flex; + align-items: center; + gap: 8px; + color: #666; + font-size: 13px; + font-weight: 500; +} + +img { + max-width: 100% !important; +} blockquote { padding: 0.2em 1.2em !important; diff --git a/app/src/main/res/raw/toggle_quote_visibility_script.js b/app/src/main/res/raw/toggle_quote_visibility_script.js index 5727a9745ec..2d19ce3f5f9 100644 --- a/app/src/main/res/raw/toggle_quote_visibility_script.js +++ b/app/src/main/res/raw/toggle_quote_visibility_script.js @@ -1,7 +1,6 @@ (function() { function changeQuoteVisibility() { const button = document.getElementById('quote-toggle-btn'); - console.log("BUTTON", button) if (!button) return; // Hide quotes initially and show toggle button @@ -14,11 +13,20 @@ button.style.display = 'block'; } - // Attach click handler to button button.onclick = function(e) { e.preventDefault(); quoteElements.forEach(el => { - el.style.display = 'block'; + el.style.display = 'block'; + // Force reload images inside quotes + el.querySelectorAll('img').forEach(img => { + if (img.src.startsWith('cid:')) { + // Store original CID, then reload + const cid = img.src; + img.src = ''; + + setTimeout(() => img.src = cid, 0); + } + }); }); button.style.display = 'none'; }; From eb7f4fa892f6ecd70c7e229b021246b8ce38c2d8 Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Mon, 2 Mar 2026 14:54:14 +0100 Subject: [PATCH 17/94] feat: Change 3 dots for text button and fix ai PR suggestions --- .../mail/ui/newMessage/NewMessageFragment.kt | 72 ++++++++----------- .../NewMessageRecipientFieldsManager.kt | 10 ++- .../mail/ui/newMessage/NewMessageViewModel.kt | 38 +++++----- .../infomaniak/mail/utils/HtmlFormatter.kt | 13 +++- .../infomaniak/mail/utils/MessageBodyUtils.kt | 14 +--- .../infomaniak/mail/utils/SignatureUtils.kt | 6 +- .../com/infomaniak/mail/utils/WebViewUtils.kt | 6 +- app/src/main/res/drawable/ic_more_horiz.xml | 28 -------- .../main/res/layout/fragment_new_message.xml | 14 ++-- app/src/main/res/raw/editor_style.css | 5 +- .../main/res/raw/replace_signature_script.js | 36 ++++++++++ app/src/main/res/raw/show_quotes_script.js | 35 +++++++++ app/src/main/res/raw/signature_margins.css | 2 +- app/src/main/res/raw/style.css | 13 ---- .../res/raw/toggle_quote_visibility_script.js | 36 ---------- 15 files changed, 158 insertions(+), 170 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_more_horiz.xml create mode 100644 app/src/main/res/raw/replace_signature_script.js create mode 100644 app/src/main/res/raw/show_quotes_script.js delete mode 100644 app/src/main/res/raw/toggle_quote_visibility_script.js diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 9881ccf01ff..d37b4ab7dd4 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -88,15 +88,16 @@ import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.UiFrom import com.infomaniak.mail.ui.newMessage.encryption.EncryptionMessageManager import com.infomaniak.mail.ui.newMessage.encryption.EncryptionViewModel import com.infomaniak.mail.utils.AccountUtils +import com.infomaniak.mail.utils.HtmlFormatter.Companion.escapeForJS import com.infomaniak.mail.utils.HtmlFormatter.Companion.getCustomDarkMode import com.infomaniak.mail.utils.HtmlFormatter.Companion.getCustomEditorStyle import com.infomaniak.mail.utils.HtmlFormatter.Companion.getCustomStyle +import com.infomaniak.mail.utils.HtmlFormatter.Companion.getReplaceSignatureScript import com.infomaniak.mail.utils.HtmlFormatter.Companion.getSignatureMarginStyle import com.infomaniak.mail.utils.HtmlFormatter.Companion.getToggleQuotesButtonVisibilityScript import com.infomaniak.mail.utils.MessageBodyUtils import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SignatureUtils -import com.infomaniak.mail.utils.WebViewUtils import com.infomaniak.mail.utils.WebViewUtils.Companion.setupNewMessageWebViewSettings import com.infomaniak.mail.utils.extensions.AttachmentExt import com.infomaniak.mail.utils.extensions.AttachmentExt.openAttachment @@ -149,7 +150,6 @@ class NewMessageFragment : Fragment() { private val attachmentAdapter inline get() = binding.attachmentsRecyclerView.adapter as AttachmentAdapter private val newMessageActivity by lazy { requireActivity() as NewMessageActivity } - private val webViewUtils by lazy { WebViewUtils(requireContext()) } @Inject lateinit var editorContentManager: EditorContentManager @@ -187,7 +187,6 @@ class NewMessageFragment : Fragment() { @Inject lateinit var dateAndTimeScheduleDialog: SelectDateAndTimeForScheduledDraftDialog - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return FragmentNewMessageBinding.inflate(inflater, container, false).also { _binding = it }.root } @@ -468,7 +467,7 @@ class NewMessageFragment : Fragment() { if (draft != null) { val isBodyEmpty = newMessageViewModel.bodyHasPlaceholder(draft.body) binding.newMessagePlaceholder.isVisible = isBodyEmpty - showKeyboardInCorrectView(isToFieldEmpty = draft.to.isEmpty(), isBodyEmpty = isBodyEmpty) + showKeyboardInCorrectView(isToFieldEmpty = draft.to.isEmpty()) binding.subjectTextField.setText(draft.subject) } else { requireActivity().apply { @@ -480,10 +479,10 @@ class NewMessageFragment : Fragment() { } } - private fun showKeyboardInCorrectView(isToFieldEmpty: Boolean, isBodyEmpty: Boolean) = with(recipientFieldsManager) { + private fun showKeyboardInCorrectView(isToFieldEmpty: Boolean) = with(recipientFieldsManager) { when (newMessageViewModel.draftMode()) { DraftMode.REPLY, - DraftMode.REPLY_ALL -> if (isBodyEmpty) focusBodyField() + DraftMode.REPLY_ALL -> focusBodyField() DraftMode.FORWARD -> focusToField() DraftMode.NEW_MAIL -> if (isToFieldEmpty) focusToField() else focusBodyField() } @@ -491,13 +490,22 @@ class NewMessageFragment : Fragment() { private fun configureUiWithDraftData(draft: Draft) = with(binding.editorWebView) { settings.setupNewMessageWebViewSettings() - webViewClient = - initWebViewClientAndBridge( - attachments = draft.attachments, - messageUid = "MESSAGE-" + draft.messageUid, - shouldLoadDistantResources = true, - navigateToNewMessageActivity = null - ) + webViewClient = initWebViewClientAndBridge( + attachments = draft.attachments, + messageUid = "MESSAGE-" + draft.messageUid, + shouldLoadDistantResources = true, + navigateToNewMessageActivity = null + ) + } + + private fun setupToggleQuotesButton() { + binding.quotesToggleButton.isVisible = newMessageViewModel.areQuotesVisible.value + binding.quotesToggleButton.setOnClickListener { + binding.editorWebView.evaluateJavascript(requireContext().getToggleQuotesButtonVisibilityScript()) { + binding.quotesToggleButton.isGone = true + newMessageViewModel.changeQuotesVisibility(areVisible = true) + } + } } private fun setupFromField(signatures: List) = with(binding) { @@ -539,36 +547,16 @@ class NewMessageFragment : Fragment() { val wrappedNewSignature = if (signature.isDummy) "" else signatureUtils.encapsulateSignatureContentWithInfomaniakClass(newSignatureHtml) - val escapedSignature = wrappedNewSignature - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\r", "\\r") - - val replaceSignatureScript = """ - (function() { - var sigElement = document.querySelector('.${MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_CLASS_NAME}'); - var newSigHtml = "$escapedSignature"; - if (sigElement) { - if (newSigHtml === "") { - sigElement.remove(); - } else { - sigElement.outerHTML = newSigHtml; - } - } else if (newSigHtml !== "") { - var quotes = document.querySelector('.${MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME}'); - if (quotes) { - quotes.insertAdjacentHTML('beforebegin', newSigHtml); - } else { - document.body.insertAdjacentHTML('beforeend', newSigHtml); - } - } - })() - """.trimIndent() + val escapedSignature = wrappedNewSignature.escapeForJS() + + val replaceSignatureScript = requireContext().getReplaceSignatureScript().format( + MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_CLASS_NAME, + escapedSignature, + MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME + ) binding.editorWebView.evaluateJavascript(replaceSignatureScript) { - newMessageViewModel.fromLiveData.value = - UiFrom(signature) + newMessageViewModel.fromLiveData.value = UiFrom(signature) addressListPopupWindow?.dismiss() } } @@ -698,7 +686,7 @@ class NewMessageFragment : Fragment() { private fun observeBodyLoader() { newMessageViewModel.editorBodyInitializer.observe(viewLifecycleOwner) { body -> editorContentManager.setContent(binding.editorWebView, body) - binding.editorWebView.addScript(requireContext().getToggleQuotesButtonVisibilityScript()) + setupToggleQuotesButton() } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageRecipientFieldsManager.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageRecipientFieldsManager.kt index 65d6cd6c209..7026f402f11 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageRecipientFieldsManager.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageRecipientFieldsManager.kt @@ -157,7 +157,15 @@ class NewMessageRecipientFieldsManager @Inject constructor(private val snackbarM fun setOnFocusChangedListeners() = with(newMessageViewModel) { binding.subjectTextField.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) fieldGotFocus(field = null) } - binding.editorWebView.setOnFocusChangeListener { _, hasFocus -> isEditorWebViewFocusedLiveData.value = hasFocus } + binding.editorWebView.setOnFocusChangeListener { _, hasFocus -> + isEditorWebViewFocusedLiveData.value = hasFocus + if (hasFocus) { + binding.editorWebView.post { + binding.editorWebView.scrollTo(0, 0) + binding.compositionNestedScrollView.scrollTo(0, 0) + } + } + } isEditorWebViewFocusedLiveData.observe(viewLifecycleOwner) { hasFocus -> if (hasFocus) fieldGotFocus(field = null) } } 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 7cd7116a16a..3139390682e 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 @@ -24,7 +24,6 @@ import android.content.ClipDescription import android.content.Intent import android.net.Uri import android.os.Parcelable -import android.util.Log import androidx.core.app.NotificationManagerCompat import androidx.core.net.MailTo import androidx.core.net.toUri @@ -88,7 +87,6 @@ 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 -import com.infomaniak.mail.utils.MessageBodyUtils.toggleButton import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SharedUtils import com.infomaniak.mail.utils.SignatureUtils @@ -223,6 +221,9 @@ class NewMessageViewModel @Inject constructor( private val _isShimmering = MutableStateFlow(true) val isShimmering: StateFlow = _isShimmering + private val _areQuotesVisible = MutableStateFlow(false) + val areQuotesVisible: StateFlow = _areQuotesVisible + //region Check mailbox existence private val exitSignal: CompletableJob = Job() @@ -560,16 +561,13 @@ class NewMessageViewModel @Inject constructor( } if (isNewMessage && !initialQuote.isNullOrEmpty()) { + _areQuotesVisible.emit(true) finalBodyContent += MessageBodyUtils.encapsulateQuotesWithInfomaniakClass(initialQuote.toString()) } - var normalizedBody = normalizeHtml(finalBodyContent) + val normalizedBody = normalizeHtml(finalBodyContent) saveSnapshot(normalizedBody) - if (bodyHasQuotes(finalBodyContent)) { - normalizedBody += toggleButton() - } - editorBodyInitializer.postValue( BodyContentPayload( content = normalizedBody, @@ -587,15 +585,11 @@ class NewMessageViewModel @Inject constructor( fun bodyHasPlaceholder(bodyHtml: String): Boolean { val body = JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml).body() - return body.text() == "" + return !body.hasText() } - fun bodyHasQuotes(bodyHtml: String): Boolean { - val bodyHasQuotes = JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml) - .getElementsByClass(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME).count() > 0 - val toggleButtonAlreadyExists = JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml) - .getElementById("quote-toggle-btn") != null - return bodyHasQuotes && !toggleButtonAlreadyExists + fun changeQuotesVisibility(areVisible: Boolean) { + _areQuotesVisible.value = areVisible } private fun normalizeHtml(html: String): String { @@ -1022,15 +1016,17 @@ class NewMessageViewModel @Inject constructor( private fun sanitizeBody(html: String): String { val doc = jsoupParseWithLog(html) - // If the user deleted the quotes text, remove the quotes div. - val bodyHasEmptyQuotes = JsoupParserUtil.jsoupParseBodyFragmentWithLog(html) - .getElementsByClass(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME).first()?.text() == "" - if (bodyHasEmptyQuotes) { + // 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() } } - // Remove the toggle button before saving - doc.getElementById("quote-toggle-btn")?.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() } @@ -1073,8 +1069,6 @@ class NewMessageViewModel @Inject constructor( } private fun isSnapshotTheSame(subjectValue: String?, uiBodyValue: String): Boolean { - Log.d("BODY VALUE", uiBodyValue) - Log.d("SNAPSHOT", snapshot?.uiBody.toString()) return snapshot?.let { draftSnapshot -> draftSnapshot.identityId == fromLiveData.value?.signature?.id?.toString() && draftSnapshot.to == toLiveData.valueOrEmpty().toSet() && diff --git a/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt b/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt index b1f910e18f5..f93026d27bd 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt @@ -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 @@ -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) @@ -224,7 +230,6 @@ class HtmlFormatter(private val html: String) { 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) @@ -235,7 +240,11 @@ class HtmlFormatter(private val html: String) { ) fun Context.getToggleQuotesButtonVisibilityScript(): String = loadScript( - R.raw.toggle_quote_visibility_script + R.raw.show_quotes_script + ) + + fun Context.getReplaceSignatureScript(): String = loadScript( + R.raw.replace_signature_script ) fun Context.getFixStyleScript(): String { 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 6f43b96b2a7..6b5486f0fb2 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt @@ -64,19 +64,7 @@ object MessageBodyUtils { ) fun encapsulateQuotesWithInfomaniakClass(quotes: String): String { - return """
$quotes
""".trimIndent() - } - - fun toggleButton(): String { - return """ - - """.trimIndent() + return """
$quotes
""".trimIndent() } suspend fun splitContentAndQuote(body: Body): SplitBody { diff --git a/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt index 65f8f0a4821..6041ff26004 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt @@ -32,6 +32,8 @@ class SignatureUtils @Inject constructor(appContext: Context) { fun encapsulateSignatureContentWithInfomaniakClass(signatureContent: String): String { val verticalMarginsCss = signatureMargins val verticalMarginAttributes = extractAttributesFromMarginCss(verticalMarginsCss) + if (verticalMarginAttributes.isNullOrEmpty()) return signatureContent + return """
$signatureContent @@ -39,7 +41,7 @@ class SignatureUtils @Inject constructor(appContext: Context) { """.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] } } diff --git a/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt index 6d2fcfb7716..1a4e02d9967 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt @@ -126,14 +126,14 @@ class WebViewUtils(context: Context) { private fun WebSettings.setupCommonWebViewSettings() { @SuppressLint("SetJavaScriptEnabled") javaScriptEnabled = true - + loadWithOverviewMode = true cacheMode = LOAD_CACHE_ELSE_NETWORK } fun WebSettings.setupThreadWebViewSettings() { setupCommonWebViewSettings() - - loadWithOverviewMode = true + + useWideViewPort = true setSupportZoom(true) builtInZoomControls = true displayZoomControls = false diff --git a/app/src/main/res/drawable/ic_more_horiz.xml b/app/src/main/res/drawable/ic_more_horiz.xml deleted file mode 100644 index 602d0daa2d0..00000000000 --- a/app/src/main/res/drawable/ic_more_horiz.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/app/src/main/res/layout/fragment_new_message.xml b/app/src/main/res/layout/fragment_new_message.xml index 4ae09d4a17a..c766a7cbb37 100644 --- a/app/src/main/res/layout/fragment_new_message.xml +++ b/app/src/main/res/layout/fragment_new_message.xml @@ -320,14 +320,16 @@ + android:padding="@dimen/marginStandardVerySmall" + android:text="@string/messageShowQuotedText" + android:visibility="gone" + app:layout_constraintStart_toStartOf="@id/postFields" + app:layout_constraintTop_toBottomOf="@id/editorWebView" + tools:visibility="visible" /> . + */ + +(function() { + var signatureElement = document.querySelector('.%s'); + var newSigHtml = "%s"; + if (signatureElement) { + if (newSigHtml === "") { + signatureElement.remove(); + } else { + signatureElement.outerHTML = newSigHtml; + } + } else if (newSigHtml !== "") { + var quotes = document.querySelector('.%s'); + if (quotes) { + quotes.insertAdjacentHTML('beforebegin', newSigHtml); + } else { + document.body.insertAdjacentHTML('beforeend', newSigHtml); + } + } +})() diff --git a/app/src/main/res/raw/show_quotes_script.js b/app/src/main/res/raw/show_quotes_script.js new file mode 100644 index 00000000000..b713cbee543 --- /dev/null +++ b/app/src/main/res/raw/show_quotes_script.js @@ -0,0 +1,35 @@ +/* + * 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 . + */ + +(function() { + const quoteElements = document.querySelectorAll('.ik_mail_quote'); + quoteElements.forEach(el => { + el.style.display = 'block'; + + // Force reload images inside quotes + el.querySelectorAll('img').forEach(img => { + if (img.src.startsWith('cid:')) { + // Store original CID, then reload + const cid = img.src; + img.src = ''; + + setTimeout(() => img.src = cid, 0); + } + }); + }); +})(); diff --git a/app/src/main/res/raw/signature_margins.css b/app/src/main/res/raw/signature_margins.css index 0b3cdb44ec1..0e4ce4ba37c 100644 --- a/app/src/main/res/raw/signature_margins.css +++ b/app/src/main/res/raw/signature_margins.css @@ -1 +1 @@ -.editorUserSignature { margin-top: 1rem; margin-bottom: 1rem; } /* Only one selector in the whole file is supported. Here it's `body` */ +.editorUserSignature { margin-top: 1rem; margin-bottom: 1rem; } /* Only one selector in the whole file is supported. */ diff --git a/app/src/main/res/raw/style.css b/app/src/main/res/raw/style.css index c08f649ecaa..70880b5bbf2 100644 --- a/app/src/main/res/raw/style.css +++ b/app/src/main/res/raw/style.css @@ -21,19 +21,6 @@ body { max-width: 100% !important; } -#quote-toggle-btn { - background: none; - border: none; - cursor: pointer; - padding: 8px 12px; - display: flex; - align-items: center; - gap: 8px; - color: #666; - font-size: 13px; - font-weight: 500; -} - img { max-width: 100% !important; } diff --git a/app/src/main/res/raw/toggle_quote_visibility_script.js b/app/src/main/res/raw/toggle_quote_visibility_script.js deleted file mode 100644 index 2d19ce3f5f9..00000000000 --- a/app/src/main/res/raw/toggle_quote_visibility_script.js +++ /dev/null @@ -1,36 +0,0 @@ -(function() { - function changeQuoteVisibility() { - const button = document.getElementById('quote-toggle-btn'); - if (!button) return; - - // Hide quotes initially and show toggle button - const quoteElements = document.querySelectorAll('.ik_mail_quote'); - - if (quoteElements.style != 'block') { - quoteElements.forEach(el => { - el.style.display = 'none'; - }); - button.style.display = 'block'; - } - - button.onclick = function(e) { - e.preventDefault(); - quoteElements.forEach(el => { - el.style.display = 'block'; - // Force reload images inside quotes - el.querySelectorAll('img').forEach(img => { - if (img.src.startsWith('cid:')) { - // Store original CID, then reload - const cid = img.src; - img.src = ''; - - setTimeout(() => img.src = cid, 0); - } - }); - }); - button.style.display = 'none'; - }; - } - - changeQuoteVisibility(); -})(); From cf78338f5c45867a76a13843da553657c9ed0ca9 Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Mon, 2 Mar 2026 15:11:40 +0100 Subject: [PATCH 18/94] fix: Fix sonar issues and add more margin to signature --- .../ui/newMessage/NewMessageRecipientFieldsManager.kt | 10 +--------- app/src/main/res/raw/editor_style.css | 2 +- app/src/main/res/raw/show_quotes_script.js | 2 +- app/src/main/res/raw/signature_margins.css | 2 +- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageRecipientFieldsManager.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageRecipientFieldsManager.kt index 7026f402f11..65d6cd6c209 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageRecipientFieldsManager.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageRecipientFieldsManager.kt @@ -157,15 +157,7 @@ class NewMessageRecipientFieldsManager @Inject constructor(private val snackbarM fun setOnFocusChangedListeners() = with(newMessageViewModel) { binding.subjectTextField.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) fieldGotFocus(field = null) } - binding.editorWebView.setOnFocusChangeListener { _, hasFocus -> - isEditorWebViewFocusedLiveData.value = hasFocus - if (hasFocus) { - binding.editorWebView.post { - binding.editorWebView.scrollTo(0, 0) - binding.compositionNestedScrollView.scrollTo(0, 0) - } - } - } + binding.editorWebView.setOnFocusChangeListener { _, hasFocus -> isEditorWebViewFocusedLiveData.value = hasFocus } isEditorWebViewFocusedLiveData.observe(viewLifecycleOwner) { hasFocus -> if (hasFocus) fieldGotFocus(field = null) } } diff --git a/app/src/main/res/raw/editor_style.css b/app/src/main/res/raw/editor_style.css index 2f9d2a238e0..1341eaab723 100644 --- a/app/src/main/res/raw/editor_style.css +++ b/app/src/main/res/raw/editor_style.css @@ -13,7 +13,7 @@ p { margin: 0; } -.ik_mail_quote, forwardContentMessage { +.ik_mail_quote, .forwardContentMessage { display: none; overflow-x: auto !important; } diff --git a/app/src/main/res/raw/show_quotes_script.js b/app/src/main/res/raw/show_quotes_script.js index b713cbee543..b4b42db1a61 100644 --- a/app/src/main/res/raw/show_quotes_script.js +++ b/app/src/main/res/raw/show_quotes_script.js @@ -17,7 +17,7 @@ */ (function() { - const quoteElements = document.querySelectorAll('.ik_mail_quote'); + const quoteElements = document.querySelectorAll('.ik_mail_quote, .forwardContentMessage'); quoteElements.forEach(el => { el.style.display = 'block'; diff --git a/app/src/main/res/raw/signature_margins.css b/app/src/main/res/raw/signature_margins.css index 0e4ce4ba37c..97fa30eda43 100644 --- a/app/src/main/res/raw/signature_margins.css +++ b/app/src/main/res/raw/signature_margins.css @@ -1 +1 @@ -.editorUserSignature { margin-top: 1rem; margin-bottom: 1rem; } /* Only one selector in the whole file is supported. */ +.editorUserSignature { margin-top: 3rem; margin-bottom: 1rem; } /* Only one selector in the whole file is supported. */ From 38a7e31208d997917890f6f12bf5fbee6262245f Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Tue, 3 Mar 2026 13:55:20 +0100 Subject: [PATCH 19/94] refactor: Refactor for PR --- .../data/cache/mailboxContent/ReplyForwardFooterManager.kt | 1 + .../com/infomaniak/mail/ui/newMessage/BodyContentPayload.kt | 2 +- .../com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt | 4 +--- app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt | 2 +- app/src/main/res/raw/signature_margins.css | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt index 9f29f544cf9..a6ef02ed183 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt @@ -53,6 +53,7 @@ class ReplyForwardFooterManager @Inject constructor(private val appContext: Cont imageElement.attr(SRC_ATTRIBUTE, "${CID_PROTOCOL}$newContentId") }, ) + document.outerHtml() } ?: "" 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 f445b35b1e1..77336cf1251 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 @@ -27,7 +27,7 @@ data class BodyContentPayload(val content: String, val type: BodyContentType) { fun emptyBody() = BodyContentPayload( content = "
", - type = BodyContentType.TEXT_PLAIN_WITHOUT_HTML + type = BodyContentType.HTML_SANITIZED ) } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index d37b4ab7dd4..8d4d97c167a 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -138,7 +138,6 @@ class NewMessageFragment : Fragment() { private val newMessageViewModel: NewMessageViewModel by activityViewModels() private val aiViewModel: AiViewModel by activityViewModels() private val encryptionViewModel: EncryptionViewModel by activityViewModels() - private var hasPlaceholder = true private val filePicker = FilePicker(fragment = this).apply { initCallback { uris -> newMessageViewModel.importAttachmentsLiveData.value = uris } @@ -445,13 +444,12 @@ class NewMessageFragment : Fragment() { private fun removePlaceholder() { binding.newMessagePlaceholder.visibility = View.GONE - hasPlaceholder = false } private fun handleFocusChanges() { newMessageViewModel.isEditorWebViewFocusedLiveData.observe(viewLifecycleOwner) { isFocused -> setToolbarEnabledStatus(isFocused) - if (isFocused && hasPlaceholder) { + if (isFocused && binding.newMessagePlaceholder.isVisible) { removePlaceholder() } } diff --git a/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt index 1a4e02d9967..0475f06e168 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt @@ -132,7 +132,7 @@ class WebViewUtils(context: Context) { fun WebSettings.setupThreadWebViewSettings() { setupCommonWebViewSettings() - + useWideViewPort = true setSupportZoom(true) builtInZoomControls = true diff --git a/app/src/main/res/raw/signature_margins.css b/app/src/main/res/raw/signature_margins.css index 97fa30eda43..bb22fc91a9d 100644 --- a/app/src/main/res/raw/signature_margins.css +++ b/app/src/main/res/raw/signature_margins.css @@ -1 +1 @@ -.editorUserSignature { margin-top: 3rem; margin-bottom: 1rem; } /* Only one selector in the whole file is supported. */ +.editorUserSignature { margin-top: 1.5rem; margin-bottom: 1rem; } /* Only one selector in the whole file is supported. */ From f438f6a534ed04d1ad5753ea4c4f9cb51e1925bd Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Tue, 3 Mar 2026 14:16:57 +0100 Subject: [PATCH 20/94] fix: Remove unnecesary space --- .../mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt index a6ef02ed183..95807dc5c79 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt @@ -53,7 +53,7 @@ class ReplyForwardFooterManager @Inject constructor(private val appContext: Cont imageElement.attr(SRC_ATTRIBUTE, "${CID_PROTOCOL}$newContentId") }, ) - + document.outerHtml() } ?: "" From 698b303da54e1f3720a3fa00dc02770f0865eb8b Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Mon, 9 Mar 2026 16:21:52 +0100 Subject: [PATCH 21/94] fix: Fix PR comments --- .../ReplyForwardFooterManager.kt | 4 +- .../mail/ui/newMessage/BodyContentPayload.kt | 3 +- .../mail/ui/newMessage/NewMessageFragment.kt | 40 ++--- .../mail/ui/newMessage/NewMessageViewModel.kt | 143 ++++++++---------- .../infomaniak/mail/utils/HtmlFormatter.kt | 19 ++- .../infomaniak/mail/utils/MessageBodyUtils.kt | 5 +- .../infomaniak/mail/utils/SignatureUtils.kt | 18 +-- .../com/infomaniak/mail/utils/WebViewUtils.kt | 2 +- .../mail/utils/extensions/Extensions.kt | 12 ++ .../main/res/layout/fragment_new_message.xml | 4 +- .../main/res/raw/add_style_with_id_script.js | 6 + app/src/main/res/raw/editor_style.css | 11 -- app/src/main/res/raw/hide_quotes_style.css | 4 + app/src/main/res/raw/show_quotes_script.js | 14 +- app/src/main/res/raw/signature_margins.css | 1 - 15 files changed, 141 insertions(+), 145 deletions(-) create mode 100644 app/src/main/res/raw/add_style_with_id_script.js create mode 100644 app/src/main/res/raw/hide_quotes_style.css delete mode 100644 app/src/main/res/raw/signature_margins.css diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt index 95807dc5c79..41c80768768 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt @@ -125,17 +125,19 @@ class ReplyForwardFooterManager @Inject constructor(private val appContext: Cont addAndEscapeTextLine("") addAlreadyEscapedBody(previousFullBody) + addAndEscapeTextLine("") }.outerHtml() } private fun assembleReplyHtmlFooter(messageReplyHeader: String, previousFullBody: String): String { - val replyRoot = """
""" + val replyRoot = """
""" return parseAndWrapElementInNewDocument(replyRoot).apply { addAndEscapeTextLine("") addAndEscapeTextLine(messageReplyHeader, endWithBr = false) addReplyBlockQuote { addAlreadyEscapedBody(previousFullBody) } + addAndEscapeTextLine("") }.outerHtml() } 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 77336cf1251..6366a2e329e 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 @@ -24,9 +24,10 @@ package com.infomaniak.mail.ui.newMessage data class BodyContentPayload(val content: String, val type: BodyContentType) { companion object { + // Add some empty lines in the body so the body focus on these lines and not in the signature fun emptyBody() = BodyContentPayload( - content = "
", + content = "

", type = BodyContentType.HTML_SANITIZED ) } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 8d4d97c167a..66e7946c791 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -92,8 +92,8 @@ import com.infomaniak.mail.utils.HtmlFormatter.Companion.escapeForJS import com.infomaniak.mail.utils.HtmlFormatter.Companion.getCustomDarkMode import com.infomaniak.mail.utils.HtmlFormatter.Companion.getCustomEditorStyle import com.infomaniak.mail.utils.HtmlFormatter.Companion.getCustomStyle +import com.infomaniak.mail.utils.HtmlFormatter.Companion.getHideQuotesStyle import com.infomaniak.mail.utils.HtmlFormatter.Companion.getReplaceSignatureScript -import com.infomaniak.mail.utils.HtmlFormatter.Companion.getSignatureMarginStyle import com.infomaniak.mail.utils.HtmlFormatter.Companion.getToggleQuotesButtonVisibilityScript import com.infomaniak.mail.utils.MessageBodyUtils import com.infomaniak.mail.utils.SentryDebug @@ -101,6 +101,7 @@ import com.infomaniak.mail.utils.SignatureUtils import com.infomaniak.mail.utils.WebViewUtils.Companion.setupNewMessageWebViewSettings import com.infomaniak.mail.utils.extensions.AttachmentExt import com.infomaniak.mail.utils.extensions.AttachmentExt.openAttachment +import com.infomaniak.mail.utils.extensions.addCssWithId import com.infomaniak.mail.utils.extensions.applySideAndBottomSystemInsets import com.infomaniak.mail.utils.extensions.applyStatusBarInsets import com.infomaniak.mail.utils.extensions.applyWindowInsetsListener @@ -439,11 +440,10 @@ class NewMessageFragment : Fragment() { if (context.isNightModeEnabled()) addCss(context.getCustomDarkMode()) addCss(context.getCustomStyle()) addCss(context.getCustomEditorStyle()) - addCss(context.getSignatureMarginStyle()) } private fun removePlaceholder() { - binding.newMessagePlaceholder.visibility = View.GONE + binding.newMessagePlaceholder.isGone = true } private fun handleFocusChanges() { @@ -479,8 +479,7 @@ class NewMessageFragment : Fragment() { private fun showKeyboardInCorrectView(isToFieldEmpty: Boolean) = with(recipientFieldsManager) { when (newMessageViewModel.draftMode()) { - DraftMode.REPLY, - DraftMode.REPLY_ALL -> focusBodyField() + DraftMode.REPLY, DraftMode.REPLY_ALL -> focusBodyField() DraftMode.FORWARD -> focusToField() DraftMode.NEW_MAIL -> if (isToFieldEmpty) focusToField() else focusBodyField() } @@ -488,11 +487,13 @@ class NewMessageFragment : Fragment() { private fun configureUiWithDraftData(draft: Draft) = with(binding.editorWebView) { settings.setupNewMessageWebViewSettings() + val alwaysShowExternalContent = localSettings.externalContent == LocalSettings.ExternalContent.ALWAYS webViewClient = initWebViewClientAndBridge( attachments = draft.attachments, messageUid = "MESSAGE-" + draft.messageUid, - shouldLoadDistantResources = true, - navigateToNewMessageActivity = null + shouldLoadDistantResources = alwaysShowExternalContent || newMessageViewModel.shouldLoadDistantResources(), + navigateToNewMessageActivity = null, + onPageFinished = { binding.editorWebView.notifyPageHasLoaded() } ) } @@ -539,11 +540,13 @@ class NewMessageFragment : Fragment() { private fun onSignatureClicked(signature: Signature) { trackNewMessageEvent(MatomoName.SwitchIdentity) + newMessageViewModel.fromLiveData.value = UiFrom(signature) + addressListPopupWindow?.dismiss() + } - // Get the New Signature HTML - val newSignatureHtml = signature.content + private fun updateBodySignature(signature: Signature) { val wrappedNewSignature = - if (signature.isDummy) "" else signatureUtils.encapsulateSignatureContentWithInfomaniakClass(newSignatureHtml) + if (signature.isDummy) "" else signatureUtils.encapsulateSignatureContentWithInfomaniakClass(signature.content) val escapedSignature = wrappedNewSignature.escapeForJS() @@ -553,10 +556,7 @@ class NewMessageFragment : Fragment() { MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME ) - binding.editorWebView.evaluateJavascript(replaceSignatureScript) { - newMessageViewModel.fromLiveData.value = UiFrom(signature) - addressListPopupWindow?.dismiss() - } + binding.editorWebView.evaluateJavascript(replaceSignatureScript, null) } private fun updateSelectedSignatureInFromField(signature: Signature) { @@ -586,8 +586,9 @@ class NewMessageFragment : Fragment() { } private fun observeFromData() = with(newMessageViewModel) { - fromLiveData.observe(viewLifecycleOwner) { (signature) -> + fromLiveData.observe(viewLifecycleOwner) { (signature, shouldUpdateBodySignature) -> updateSelectedSignatureInFromField(signature) + if (shouldUpdateBodySignature) updateBodySignature(signature) signatureAdapter.updateSelectedSignature(signature.id) } } @@ -681,10 +682,13 @@ class NewMessageFragment : Fragment() { } } - private fun observeBodyLoader() { - newMessageViewModel.editorBodyInitializer.observe(viewLifecycleOwner) { body -> + private fun observeBodyLoader() = with(newMessageViewModel) { + editorBodyInitializer.observe(viewLifecycleOwner) { body -> editorContentManager.setContent(binding.editorWebView, body) - setupToggleQuotesButton() + if (bodyHasQuotes(body.content)) { + binding.editorWebView.addCssWithId(requireContext().getHideQuotesStyle(), "quote-visibility") + setupToggleQuotesButton() + } } } 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 3139390682e..6ecfc3230e2 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 @@ -228,24 +228,19 @@ class NewMessageViewModel @Inject constructor( private val exitSignal: CompletableJob = Job() private val mailboxRefFlow = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST + replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST ) - private val _currentMailboxFlow: Flow = mailboxRefFlow - .mapLatest { - val mailbox = mailboxController.getMailbox(it.userId, it.mailboxId) - if (mailbox == null) { - exitSignal.complete() - awaitCancellation() - } - mailbox + private val _currentMailboxFlow: Flow = mailboxRefFlow.mapLatest { + val mailbox = mailboxController.getMailbox(it.userId, it.mailboxId) + if (mailbox == null) { + exitSignal.complete() + awaitCancellation() } - .shareIn(viewModelScope, SharingStarted.Lazily, replay = 1) + mailbox + }.shareIn(viewModelScope, SharingStarted.Lazily, replay = 1) - val currentUserIdFlow = _currentMailboxFlow - .map { it.userId } - .stateIn(viewModelScope, SharingStarted.Eagerly, null) + val currentUserIdFlow = _currentMailboxFlow.map { it.userId }.stateIn(viewModelScope, SharingStarted.Eagerly, null) suspend fun currentMailbox() = _currentMailboxFlow.first() @@ -311,9 +306,7 @@ class NewMessageViewModel @Inject constructor( val draft: Draft? = runCatching { - signatures = currentMailbox().signatures - .also { signaturesCount = it.count() } - .toMutableList() + signatures = currentMailbox().signatures.also { signaturesCount = it.count() }.toMutableList() .apply { add(index = 0, element = Signature.getDummySignature(appContext, email = currentMailbox().email)) } isNewMessage = !arrivedFromExistingDraft && draftLocalUuid == null @@ -323,7 +316,6 @@ class NewMessageViewModel @Inject constructor( }.getOrNull() draft?.let { - it.flagRecipientsAsAutomaticallyEntered() dismissNotification() @@ -345,7 +337,7 @@ class NewMessageViewModel @Inject constructor( draft.identityId = currentMailbox().getDefaultSignatureWithFallback().id.toString() } val contentType = - if (draft.mimeType == Utils.TEXT_PLAIN) BodyContentType.TEXT_PLAIN_WITH_HTML else BodyContentType.HTML_SANITIZED + if (draft.mimeType == Utils.TEXT_PLAIN) BodyContentType.TEXT_PLAIN_WITH_HTML else BodyContentType.HTML_UNSANITIZED initialBody = BodyContentPayload(draft.body, contentType) return draft } @@ -361,16 +353,14 @@ class NewMessageViewModel @Inject constructor( when (draftMode) { DraftMode.NEW_MAIL -> recipient?.let { to = realmListOf(it) } DraftMode.REPLY, DraftMode.REPLY_ALL, DraftMode.FORWARD -> { - previousMessageUid - ?.let { uid -> MessageController.getMessage(uid, realm) } - ?.let { message -> - val (fullMessage, hasFailedFetching) = DraftController.fetchHeavyDataIfNeeded(message, realm) - previousMessage = fullMessage + previousMessageUid?.let { uid -> MessageController.getMessage(uid, realm) }?.let { message -> + val (fullMessage, hasFailedFetching) = DraftController.fetchHeavyDataIfNeeded(message, realm) + previousMessage = fullMessage - if (hasFailedFetching) return@let + if (hasFailedFetching) return@let - setReplyForwardDraftValues(draft = this, fullMessage) - } + setReplyForwardDraftValues(draft = this, fullMessage) + } } } @@ -383,6 +373,23 @@ class NewMessageViewModel @Inject constructor( } populateWithExternalMailDataIfNeeded(draft = this, intent) + + val finalBodyContent = getFinalBodyContent() + + initialBody = BodyContentPayload(finalBodyContent, BodyContentType.HTML_UNSANITIZED) + } + + private fun getFinalBodyContent(): String { + var finalBodyContent = initialBody.content + if (!initialSignature.isNullOrEmpty()) { + finalBodyContent += initialSignature + } + if (!initialQuote.isNullOrEmpty()) { + _areQuotesVisible.value = true + finalBodyContent += initialQuote + } + + return finalBodyContent } private suspend fun setReplyForwardDraftValues(draft: Draft, fullMessage: Message) { @@ -429,7 +436,6 @@ class NewMessageViewModel @Inject constructor( private fun splitSignatureAndQuoteFromBody(draft: Draft) { val remoteBody = draft.body - Log.d("DRAFT BODY", remoteBody) if (remoteBody.isEmpty()) return val (body, signature, quote) = when (draft.mimeType) { @@ -552,26 +558,11 @@ class NewMessageViewModel @Inject constructor( attachmentsLiveData.postValue(attachments) - var finalBodyContent = initialBody.content - val signatureHtml = draftSignature?.takeIf { !it.isDummy }?.content - val wrappedSignature = signatureHtml?.let { signatureUtils.encapsulateSignatureContentWithInfomaniakClass(it) } - - if (isNewMessage && wrappedSignature != null) { - finalBodyContent += wrappedSignature - } - - if (isNewMessage && !initialQuote.isNullOrEmpty()) { - _areQuotesVisible.emit(true) - finalBodyContent += MessageBodyUtils.encapsulateQuotesWithInfomaniakClass(initialQuote.toString()) - } - - val normalizedBody = normalizeHtml(finalBodyContent) - saveSnapshot(normalizedBody) + saveSnapshot(initialBody.content) editorBodyInitializer.postValue( BodyContentPayload( - content = normalizedBody, - type = BodyContentType.HTML_SANITIZED + content = initialBody.content, type = BodyContentType.HTML_UNSANITIZED ) ) @@ -584,7 +575,11 @@ class NewMessageViewModel @Inject constructor( } fun bodyHasPlaceholder(bodyHtml: String): Boolean { - val body = JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml).body() + val body = JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml) + // Remove signature and quotes from body + // body.select(".${MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_CLASS_NAME}")?.remove() + // body.select(".${MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME}")?.remove() + // Check if there is any text left return !body.hasText() } @@ -592,12 +587,6 @@ class NewMessageViewModel @Inject constructor( _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") @@ -666,9 +655,11 @@ class NewMessageViewModel @Inject constructor( } if (hasExtra(Intent.EXTRA_STREAM)) { - (parcelableExtra(Intent.EXTRA_STREAM) as? Uri) - ?.let { importAttachments(currentAttachments = draft.attachments, uris = listOf(it)) } - ?.let(draft.attachments::addAll) + (parcelableExtra(Intent.EXTRA_STREAM) as? Uri)?.let { + importAttachments( + currentAttachments = draft.attachments, uris = listOf(it) + ) + }?.let(draft.attachments::addAll) } } @@ -714,15 +705,11 @@ class NewMessageViewModel @Inject constructor( val mailToIntent = runCatching { MailTo.parse(uri!!) }.getOrNull() if (mailToIntent == null && intent?.hasExtra(Intent.EXTRA_EMAIL) != true) return - val splitTo = mailToIntent?.to?.splitToRecipientList() - ?: intent?.getRecipientsFromIntent(Intent.EXTRA_EMAIL) - ?: emptyList() - val splitCc = mailToIntent?.cc?.splitToRecipientList() - ?: intent?.getRecipientsFromIntent(Intent.EXTRA_CC) - ?: emptyList() - val splitBcc = mailToIntent?.bcc?.splitToRecipientList() - ?: intent?.getRecipientsFromIntent(Intent.EXTRA_BCC) - ?: emptyList() + val splitTo = + mailToIntent?.to?.splitToRecipientList() ?: intent?.getRecipientsFromIntent(Intent.EXTRA_EMAIL) ?: emptyList() + val splitCc = mailToIntent?.cc?.splitToRecipientList() ?: intent?.getRecipientsFromIntent(Intent.EXTRA_CC) ?: emptyList() + val splitBcc = + mailToIntent?.bcc?.splitToRecipientList() ?: intent?.getRecipientsFromIntent(Intent.EXTRA_BCC) ?: emptyList() draft.apply { to.addAll(splitTo) @@ -1008,8 +995,7 @@ 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) { - val normalizedBody = normalizeHtml(body) - copyFromRealm().saveSnapshot(normalizedBody) + copyFromRealm().saveSnapshot(body) isNewMessage = false } } @@ -1017,19 +1003,22 @@ class NewMessageViewModel @Inject constructor( 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()) { + + if (bodyHasQuotes(html)) { 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() } + fun bodyHasQuotes(body: String): Boolean { + val replyQuotes = JsoupParserUtil.jsoupParseBodyFragmentWithLog(body) + .getElementsByClass(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME) + val forwardQuotes = JsoupParserUtil.jsoupParseBodyFragmentWithLog(body) + .getElementsByClass(MessageBodyUtils.INFOMANIAK_FORWARD_QUOTE_HTML_CLASS_NAME) + return replyQuotes.isNotEmpty() && replyQuotes.hasText() || forwardQuotes.isNotEmpty() && forwardQuotes.hasText() + } + private fun Draft.updateDraftAttachmentsWithLiveData(uiAttachments: List, step: String) { /** @@ -1039,9 +1028,8 @@ class NewMessageViewModel @Inject constructor( * - none got removed (their quantity is the same in UI and in Realm), * Then it means the Attachments list hasn't been edited by the user, so we have nothing to do here. */ - val isForwardingUneditedAttachmentsList = draftMode == DraftMode.FORWARD && - uiAttachments.all { it.attachmentUploadStatus == AttachmentUploadStatus.UPLOADED } && - uiAttachments.count() == attachments.count() + val isForwardingUneditedAttachmentsList = + draftMode == DraftMode.FORWARD && uiAttachments.all { it.attachmentUploadStatus == AttachmentUploadStatus.UPLOADED } && uiAttachments.count() == attachments.count() if (isForwardingUneditedAttachmentsList) return val updatedAttachments = uiAttachments.map { uiAttachment -> @@ -1133,8 +1121,7 @@ class NewMessageViewModel @Inject constructor( } enum class ImportationResult { - SUCCESS, - ATTACHMENTS_TOO_BIG, + SUCCESS, ATTACHMENTS_TOO_BIG, } data class MailboxRef(val userId: Int, val mailboxId: Int) diff --git a/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt b/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt index f93026d27bd..6166b817862 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt @@ -230,7 +230,7 @@ class HtmlFormatter(private val html: String) { listOf(PRIMARY_COLOR_CODE to getAttributeColor(RAndroid.attr.colorPrimary)) ) - fun Context.getSignatureMarginStyle(): String = loadCss(R.raw.signature_margins) + fun Context.getHideQuotesStyle(): String = loadCss(R.raw.hide_quotes_style) fun Context.getPrintMailStyle(): String = loadCss(R.raw.print_email) @@ -239,13 +239,18 @@ class HtmlFormatter(private val html: String) { listOf("MESSAGE_SELECTOR" to "#$KMAIL_MESSAGE_ID") ) - fun Context.getToggleQuotesButtonVisibilityScript(): String = loadScript( - R.raw.show_quotes_script - ) + private var cachedToggleQuotesScript: String? = null + fun Context.getToggleQuotesButtonVisibilityScript(): String { + return cachedToggleQuotesScript ?: loadScript(R.raw.show_quotes_script).also { cachedToggleQuotesScript = it } + } - fun Context.getReplaceSignatureScript(): String = loadScript( - R.raw.replace_signature_script - ) + fun Context.getAddStyleWithIdScript(): String = loadScript(R.raw.add_style_with_id_script) + + private var cachedReplaceSignatureScript: String? = null + fun Context.getReplaceSignatureScript(): String { + return cachedReplaceSignatureScript + ?: loadScript(R.raw.replace_signature_script).also { cachedReplaceSignatureScript = it } + } fun Context.getFixStyleScript(): String { return loadScript(R.raw.fix_email_style) 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 6b5486f0fb2..77d756539cd 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/MessageBodyUtils.kt @@ -63,8 +63,9 @@ object MessageBodyUtils { "[name=\"quote\"]", // GMX ) - fun encapsulateQuotesWithInfomaniakClass(quotes: String): String { - return """
$quotes
""".trimIndent() + fun addSpaceAtTheEndOfQuotes(quotes: String): String { + // This will help the user to delete the quotes completely + return """$quotes
""" } suspend fun splitContentAndQuote(body: Body): SplitBody { diff --git a/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt index 6041ff26004..96c1bbabb28 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/SignatureUtils.kt @@ -17,31 +17,17 @@ */ 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 @Singleton -class SignatureUtils @Inject constructor(appContext: Context) { - - private val signatureMargins by lazy { appContext.loadCss(R.raw.signature_margins) } - +class SignatureUtils @Inject constructor() { fun encapsulateSignatureContentWithInfomaniakClass(signatureContent: String): String { - val verticalMarginsCss = signatureMargins - val verticalMarginAttributes = extractAttributesFromMarginCss(verticalMarginsCss) - if (verticalMarginAttributes.isNullOrEmpty()) return signatureContent - return """ -
+
$signatureContent
""".trimIndent() } - - private fun extractAttributesFromMarginCss(verticalMarginsCss: String): String? { - return Regex("""\{(.*)\}""").find(verticalMarginsCss)?.groupValues[1] - } } diff --git a/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt b/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt index 0475f06e168..c20fda69021 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/WebViewUtils.kt @@ -126,7 +126,6 @@ class WebViewUtils(context: Context) { private fun WebSettings.setupCommonWebViewSettings() { @SuppressLint("SetJavaScriptEnabled") javaScriptEnabled = true - loadWithOverviewMode = true cacheMode = LOAD_CACHE_ELSE_NETWORK } @@ -134,6 +133,7 @@ class WebViewUtils(context: Context) { setupCommonWebViewSettings() useWideViewPort = true + loadWithOverviewMode = true setSupportZoom(true) builtInZoomControls = true displayZoomControls = false diff --git a/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt b/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt index b70f87b6126..e46b63103c1 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/extensions/Extensions.kt @@ -79,6 +79,7 @@ import com.infomaniak.core.sentry.SentryLog import com.infomaniak.core.ui.showToast import com.infomaniak.dragdropswiperecyclerview.DragDropSwipeRecyclerView import com.infomaniak.lib.login.InfomaniakLogin +import com.infomaniak.lib.richhtmleditor.RichHtmlEditorWebView import com.infomaniak.mail.BuildConfig import com.infomaniak.mail.R import com.infomaniak.mail.data.LocalSettings.ThreadDensity @@ -110,6 +111,7 @@ import com.infomaniak.mail.ui.main.thread.ThreadFragment.HeaderState import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.UiRecipients import com.infomaniak.mail.utils.AccountUtils import com.infomaniak.mail.utils.ApiErrorException +import com.infomaniak.mail.utils.HtmlFormatter.Companion.getAddStyleWithIdScript import com.infomaniak.mail.utils.JsoupParserUtil.jsoupParseWithLog import com.infomaniak.mail.utils.UiUtils.animateColorChange import com.infomaniak.mail.utils.Utils @@ -647,4 +649,14 @@ fun WebView.enableAlgorithmicDarkening(isEnabled: Boolean) { } } +fun RichHtmlEditorWebView.addCssWithId(css: String, styleId: String) { + val escapedCss = css.replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\n", "\\n") + .replace("\r", "") + + val js = context.getAddStyleWithIdScript().format(styleId, escapedCss) + evaluateJavascript(js, null) +} + fun Data.getLongOrNull(key: String) = getLong(key, 0L).run { if (this == 0L) null else this } diff --git a/app/src/main/res/layout/fragment_new_message.xml b/app/src/main/res/layout/fragment_new_message.xml index c766a7cbb37..92131f84f97 100644 --- a/app/src/main/res/layout/fragment_new_message.xml +++ b/app/src/main/res/layout/fragment_new_message.xml @@ -323,11 +323,11 @@ style="@style/TextButton" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="10dp" + android:layout_marginStart="@dimen/marginStandardMedium" android:padding="@dimen/marginStandardVerySmall" android:text="@string/messageShowQuotedText" android:visibility="gone" - app:layout_constraintStart_toStartOf="@id/postFields" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/editorWebView" tools:visibility="visible" /> diff --git a/app/src/main/res/raw/add_style_with_id_script.js b/app/src/main/res/raw/add_style_with_id_script.js new file mode 100644 index 00000000000..094a1fc1914 --- /dev/null +++ b/app/src/main/res/raw/add_style_with_id_script.js @@ -0,0 +1,6 @@ +(function() { + var style = document.createElement('style'); + style.id="%s"; + style.textContent="%s"; + document.head.appendChild(style); +})() diff --git a/app/src/main/res/raw/editor_style.css b/app/src/main/res/raw/editor_style.css index 1341eaab723..212270a6280 100644 --- a/app/src/main/res/raw/editor_style.css +++ b/app/src/main/res/raw/editor_style.css @@ -6,14 +6,3 @@ body { margin-top: 0px; margin-bottom: 1rem; } - -p { - min-height: 1.4em; - line-height: 1.4em; - margin: 0; -} - -.ik_mail_quote, .forwardContentMessage { - display: none; - overflow-x: auto !important; -} diff --git a/app/src/main/res/raw/hide_quotes_style.css b/app/src/main/res/raw/hide_quotes_style.css new file mode 100644 index 00000000000..7537ab08f78 --- /dev/null +++ b/app/src/main/res/raw/hide_quotes_style.css @@ -0,0 +1,4 @@ +.ik_mail_quote, .forwardContentMessage { + display: none; + overflow-x: auto !important; +} diff --git a/app/src/main/res/raw/show_quotes_script.js b/app/src/main/res/raw/show_quotes_script.js index b4b42db1a61..8d4350719ab 100644 --- a/app/src/main/res/raw/show_quotes_script.js +++ b/app/src/main/res/raw/show_quotes_script.js @@ -17,19 +17,19 @@ */ (function() { - const quoteElements = document.querySelectorAll('.ik_mail_quote, .forwardContentMessage'); - quoteElements.forEach(el => { - el.style.display = 'block'; + var style = document.getElementById("quote-visibility") - // Force reload images inside quotes - el.querySelectorAll('img').forEach(img => { + if (style) { + style.remove() + + // Handle CID image reload when showing + document.querySelectorAll('.ik_mail_quote img, .forwardContentMessage img').forEach(img => { if (img.src.startsWith('cid:')) { // Store original CID, then reload const cid = img.src; img.src = ''; - setTimeout(() => img.src = cid, 0); } }); - }); + } })(); diff --git a/app/src/main/res/raw/signature_margins.css b/app/src/main/res/raw/signature_margins.css deleted file mode 100644 index bb22fc91a9d..00000000000 --- a/app/src/main/res/raw/signature_margins.css +++ /dev/null @@ -1 +0,0 @@ -.editorUserSignature { margin-top: 1.5rem; margin-bottom: 1rem; } /* Only one selector in the whole file is supported. */ From 6610621e3e4c48dcde985933a1f877bc7ed418aa Mon Sep 17 00:00:00 2001 From: Sol Rubado Date: Tue, 10 Mar 2026 16:29:46 +0100 Subject: [PATCH 22/94] fix: Fix PR comments --- .../ReplyForwardFooterManager.kt | 2 +- .../ui/main/thread/MessageWebViewClient.kt | 25 ---- .../mail/ui/newMessage/BodyContentPayload.kt | 4 +- .../mail/ui/newMessage/NewMessageFragment.kt | 43 ++++--- .../mail/ui/newMessage/NewMessageViewModel.kt | 117 +++++++++++------- .../infomaniak/mail/utils/HtmlFormatter.kt | 21 +--- .../main/res/layout/fragment_new_message.xml | 4 +- app/src/main/res/layout/item_message.xml | 9 +- app/src/main/res/raw/editor_style.css | 1 + app/src/main/res/raw/hide_quotes_style.css | 1 - .../main/res/raw/replace_signature_script.js | 8 +- app/src/main/res/raw/show_quotes_script.js | 2 +- app/src/main/res/raw/style.css | 14 --- app/src/main/res/values/styles.xml | 10 ++ 14 files changed, 122 insertions(+), 139 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt index 41c80768768..9ec222568ef 100644 --- a/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt +++ b/app/src/main/java/com/infomaniak/mail/data/cache/mailboxContent/ReplyForwardFooterManager.kt @@ -130,7 +130,7 @@ class ReplyForwardFooterManager @Inject constructor(private val appContext: Cont } private fun assembleReplyHtmlFooter(messageReplyHeader: String, previousFullBody: String): String { - val replyRoot = """
""" + val replyRoot = """
""" return parseAndWrapElementInNewDocument(replyRoot).apply { addAndEscapeTextLine("") addAndEscapeTextLine(messageReplyHeader, endWithBr = false) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/MessageWebViewClient.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/MessageWebViewClient.kt index 7976f15aa6e..1b2efeda752 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/MessageWebViewClient.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/MessageWebViewClient.kt @@ -25,16 +25,12 @@ import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient import com.infomaniak.core.ui.showToast -import com.infomaniak.core.ui.view.toDp import com.infomaniak.mail.R import com.infomaniak.mail.data.api.ApiRepository import com.infomaniak.mail.data.models.Attachment import com.infomaniak.mail.utils.LocalStorageUtils import com.infomaniak.mail.utils.Utils import com.infomaniak.mail.utils.Utils.runCatchingRealm -import com.infomaniak.mail.utils.WebViewVersionUtils.getWebViewVersionData -import io.sentry.Sentry -import io.sentry.SentryLevel import kotlinx.coroutines.runBlocking import java.io.ByteArrayInputStream @@ -107,27 +103,6 @@ class MessageWebViewClient( override fun onPageFinished(webView: WebView, url: String?) { runCatchingRealm { - val widthInDp = webView.width.toDp(webView) - if (widthInDp <= 0) { - val versionData = getWebViewVersionData(context) - - Sentry.captureMessage("Zero width webview detected onPageFinished which prevents message width's normalization") { scope -> - scope.level = SentryLevel.WARNING - scope.setExtra("width", webView.width.toString()) - scope.setExtra("measuredWidth", webView.measuredWidth.toString()) - scope.setExtra("height", webView.height.toString()) - scope.setExtra("measuredHeight", webView.measuredHeight.toString()) - scope.setTag( - "webview version", - "${versionData?.webViewPackageName}: ${versionData?.versionName} - ${versionData?.majorVersion}" - ) - scope.setTag("visibility", webView.visibility.toString()) - scope.setTag("messageUid", messageUid) - scope.setTag("shouldLoadDistantResources", shouldLoadDistantResources.toString()) - } - } - - webView.loadUrl("javascript:removeAllProperties(); normalizeMessageWidth($widthInDp, '$messageUid')") onPageFinished?.invoke() } } 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 6366a2e329e..618f5d2b14f 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 @@ -27,8 +27,8 @@ data class BodyContentPayload(val content: String, val type: BodyContentType) { // Add some empty lines in the body so the body focus on these lines and not in the signature fun emptyBody() = BodyContentPayload( - content = "

", - type = BodyContentType.HTML_SANITIZED + content = "

", + type = BodyContentType.HTML_UNSANITIZED ) } } diff --git a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt index 66e7946c791..a38a9b62f91 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt @@ -58,6 +58,7 @@ import com.infomaniak.lib.richhtmleditor.StatusCommand.ITALIC import com.infomaniak.lib.richhtmleditor.StatusCommand.STRIKE_THROUGH import com.infomaniak.lib.richhtmleditor.StatusCommand.UNDERLINE import com.infomaniak.lib.richhtmleditor.StatusCommand.UNORDERED_LIST +import com.infomaniak.lib.richhtmleditor.looselyEscapeAsStringLiteralForJs import com.infomaniak.mail.MatomoMail.MatomoName import com.infomaniak.mail.MatomoMail.trackAttachmentActionsEvent import com.infomaniak.mail.MatomoMail.trackNewMessageEvent @@ -88,13 +89,12 @@ import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.UiFrom import com.infomaniak.mail.ui.newMessage.encryption.EncryptionMessageManager import com.infomaniak.mail.ui.newMessage.encryption.EncryptionViewModel import com.infomaniak.mail.utils.AccountUtils -import com.infomaniak.mail.utils.HtmlFormatter.Companion.escapeForJS import com.infomaniak.mail.utils.HtmlFormatter.Companion.getCustomDarkMode import com.infomaniak.mail.utils.HtmlFormatter.Companion.getCustomEditorStyle import com.infomaniak.mail.utils.HtmlFormatter.Companion.getCustomStyle import com.infomaniak.mail.utils.HtmlFormatter.Companion.getHideQuotesStyle import com.infomaniak.mail.utils.HtmlFormatter.Companion.getReplaceSignatureScript -import com.infomaniak.mail.utils.HtmlFormatter.Companion.getToggleQuotesButtonVisibilityScript +import com.infomaniak.mail.utils.HtmlFormatter.Companion.getShowQuotesScript import com.infomaniak.mail.utils.MessageBodyUtils import com.infomaniak.mail.utils.SentryDebug import com.infomaniak.mail.utils.SignatureUtils @@ -135,6 +135,10 @@ class NewMessageFragment : Fragment() { // extras aren't yet initialized, so we don't use the `navArgs` here. requireActivity().intent?.extras?.let(NewMessageActivityArgs::fromBundle) ?: NewMessageActivityArgs() } + private val replaceSignatureScript by lazy { requireContext().getReplaceSignatureScript() } + private val showQuotesScript by lazy { requireContext().getShowQuotesScript() } + private val hideQuotesStyle by lazy { requireContext().getHideQuotesStyle() } + private val newMessageFragmentArgs: NewMessageFragmentArgs by navArgs() private val newMessageViewModel: NewMessageViewModel by activityViewModels() private val aiViewModel: AiViewModel by activityViewModels() @@ -219,6 +223,7 @@ class NewMessageFragment : Fragment() { observeImportAttachmentsResult() observeBodyLoader() observeShimmering() + observeQuotesVisibility() setupBackActionHandler() @@ -463,7 +468,7 @@ class NewMessageFragment : Fragment() { if (initResult.value == null) { initDraftAndViewModel(intent = requireActivity().intent).observe(viewLifecycleOwner) { draft -> if (draft != null) { - val isBodyEmpty = newMessageViewModel.bodyHasPlaceholder(draft.body) + val isBodyEmpty = newMessageViewModel.bodyIsEmpty(draft.body) binding.newMessagePlaceholder.isVisible = isBodyEmpty showKeyboardInCorrectView(isToFieldEmpty = draft.to.isEmpty()) binding.subjectTextField.setText(draft.subject) @@ -493,16 +498,21 @@ class NewMessageFragment : Fragment() { messageUid = "MESSAGE-" + draft.messageUid, shouldLoadDistantResources = alwaysShowExternalContent || newMessageViewModel.shouldLoadDistantResources(), navigateToNewMessageActivity = null, - onPageFinished = { binding.editorWebView.notifyPageHasLoaded() } + onPageFinished = { notifyPageHasLoaded() } ) } - private fun setupToggleQuotesButton() { - binding.quotesToggleButton.isVisible = newMessageViewModel.areQuotesVisible.value - binding.quotesToggleButton.setOnClickListener { - binding.editorWebView.evaluateJavascript(requireContext().getToggleQuotesButtonVisibilityScript()) { - binding.quotesToggleButton.isGone = true - newMessageViewModel.changeQuotesVisibility(areVisible = true) + private fun observeQuotesVisibility() = viewLifecycleOwner.lifecycleScope.launch { + // Don't show button if quotes are already visible + newMessageViewModel.isQuotesButtonVisible.collect { isVisible -> + binding.quotesToggleButton.isVisible = isVisible + } + } + + private fun setupToggleQuotesButton() = with(binding) { + quotesToggleButton.setOnClickListener { + editorWebView.evaluateJavascript(showQuotesScript) { + newMessageViewModel.changeQuotesButtonVisibility(isVisible = false) } } } @@ -545,12 +555,9 @@ class NewMessageFragment : Fragment() { } private fun updateBodySignature(signature: Signature) { - val wrappedNewSignature = - if (signature.isDummy) "" else signatureUtils.encapsulateSignatureContentWithInfomaniakClass(signature.content) - - val escapedSignature = wrappedNewSignature.escapeForJS() + val escapedSignature = if (signature.isDummy) "" else looselyEscapeAsStringLiteralForJs(signature.content) - val replaceSignatureScript = requireContext().getReplaceSignatureScript().format( + val replaceSignatureScript = replaceSignatureScript.format( MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_CLASS_NAME, escapedSignature, MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME @@ -685,10 +692,8 @@ class NewMessageFragment : Fragment() { private fun observeBodyLoader() = with(newMessageViewModel) { editorBodyInitializer.observe(viewLifecycleOwner) { body -> editorContentManager.setContent(binding.editorWebView, body) - if (bodyHasQuotes(body.content)) { - binding.editorWebView.addCssWithId(requireContext().getHideQuotesStyle(), "quote-visibility") - setupToggleQuotesButton() - } + binding.editorWebView.addCssWithId(hideQuotesStyle, "quote-visibility") + setupToggleQuotesButton() } } 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 6ecfc3230e2..3fa61c67192 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 @@ -24,6 +24,7 @@ import android.content.ClipDescription import android.content.Intent import android.net.Uri import android.os.Parcelable +import android.util.Log import androidx.core.app.NotificationManagerCompat import androidx.core.net.MailTo import androidx.core.net.toUri @@ -162,8 +163,6 @@ class NewMessageViewModel @Inject constructor( //region Initial data private var initialBody: BodyContentPayload = BodyContentPayload.emptyBody() - private var initialSignature: String? = null - private var initialQuote: String? = null //endregion //region UI data @@ -221,8 +220,9 @@ class NewMessageViewModel @Inject constructor( private val _isShimmering = MutableStateFlow(true) val isShimmering: StateFlow = _isShimmering - private val _areQuotesVisible = MutableStateFlow(false) - val areQuotesVisible: StateFlow = _areQuotesVisible + private val _isQuotesButtonVisible = MutableStateFlow(false) + val isQuotesButtonVisible: StateFlow = _isQuotesButtonVisible + private var hasQuotes: Boolean = false //region Check mailbox existence private val exitSignal: CompletableJob = Job() @@ -324,6 +324,9 @@ class NewMessageViewModel @Inject constructor( realm.write { DraftController.upsertDraftBlocking(it, realm = this) } it.initLiveData(signatures) _isShimmering.emit(false) + if (hasQuotes) { + changeQuotesButtonVisibility(isVisible = true) + } initResult.postValue(InitResult(it, signatures)) } @@ -336,8 +339,10 @@ class NewMessageViewModel @Inject constructor( if (draft.identityId.isNullOrBlank()) { draft.identityId = currentMailbox().getDefaultSignatureWithFallback().id.toString() } - val contentType = - if (draft.mimeType == Utils.TEXT_PLAIN) BodyContentType.TEXT_PLAIN_WITH_HTML else BodyContentType.HTML_UNSANITIZED + val contentType = if (draft.mimeType == Utils.TEXT_PLAIN) + BodyContentType.TEXT_PLAIN_WITHOUT_HTML + else + BodyContentType.HTML_UNSANITIZED initialBody = BodyContentPayload(draft.body, contentType) return draft } @@ -350,6 +355,7 @@ class NewMessageViewModel @Inject constructor( initLocalValues(mimeType = ClipDescription.MIMETYPE_TEXT_HTML) saveNavArgsToSavedState(localUuid) + var initialQuote: String? = null when (draftMode) { DraftMode.NEW_MAIL -> recipient?.let { to = realmListOf(it) } DraftMode.REPLY, DraftMode.REPLY_ALL, DraftMode.FORWARD -> { @@ -359,11 +365,13 @@ class NewMessageViewModel @Inject constructor( if (hasFailedFetching) return@let - setReplyForwardDraftValues(draft = this, fullMessage) + initializeReplyForwardDraftValues(draft = this, fullMessage) + initialQuote = draftInitManager.createQuote(draftMode, fullMessage, attachments) } } } + var initialSignature: String? = null with(draftInitManager) { val signature = chooseSignature(currentMailbox().email, signatures, draftMode, previousMessage) setSignatureIdentity(signature) @@ -374,33 +382,34 @@ class NewMessageViewModel @Inject constructor( populateWithExternalMailDataIfNeeded(draft = this, intent) - val finalBodyContent = getFinalBodyContent() + val finalBodyContent = getFinalBodyContent(initialSignature, initialQuote) initialBody = BodyContentPayload(finalBodyContent, BodyContentType.HTML_UNSANITIZED) } - private fun getFinalBodyContent(): String { + private fun getFinalBodyContent(initialSignature: String?, initialQuote: String?): String { var finalBodyContent = initialBody.content + if (!initialSignature.isNullOrEmpty()) { finalBodyContent += initialSignature } if (!initialQuote.isNullOrEmpty()) { - _areQuotesVisible.value = true finalBodyContent += initialQuote + hasQuotes = true } return finalBodyContent } - private suspend fun setReplyForwardDraftValues(draft: Draft, fullMessage: Message) { + /** + * Initializes some [draft] values related to reply, reply all and forward, when creating a new draft from scratch. + */ + private suspend fun initializeReplyForwardDraftValues(draft: Draft, fullMessage: Message) { with(draftInitManager) { draft.setPreviousMessage(draftMode = draftMode, previousMessage = fullMessage) } - val quote = draftInitManager.createQuote(draftMode, fullMessage, draft.attachments) - if (quote != null) initialQuote = quote - if (fullMessage.body == null) { SentryLog.e(TAG, "The message we're trying to reply to has an unexpected null body") { scope -> scope.setExtra("Message resource", fullMessage.resource) @@ -560,11 +569,7 @@ class NewMessageViewModel @Inject constructor( saveSnapshot(initialBody.content) - editorBodyInitializer.postValue( - BodyContentPayload( - content = initialBody.content, type = BodyContentType.HTML_UNSANITIZED - ) - ) + editorBodyInitializer.postValue(initialBody) if (cc.isNotEmpty() || bcc.isNotEmpty()) { otherRecipientsFieldsAreEmpty.postValue(false) @@ -574,17 +579,44 @@ class NewMessageViewModel @Inject constructor( isEncryptionActivated.postValue(isEncrypted) } - fun bodyHasPlaceholder(bodyHtml: String): Boolean { - val body = JsoupParserUtil.jsoupParseBodyFragmentWithLog(bodyHtml) - // Remove signature and quotes from body - // body.select(".${MessageBodyUtils.INFOMANIAK_SIGNATURE_HTML_CLASS_NAME}")?.remove() - // body.select(".${MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME}")?.remove() - // Check if there is any text left - return !body.hasText() + fun bodyIsEmpty(bodyHtml: String): Boolean { + val body = getBodyWithoutSignatureAndQuotes(bodyHtml) + return body.isEmpty() + } + + private fun getBodyWithoutSignatureAndQuotes(draftBody: String): String { + + fun Document.split(divClassName: String, defaultValue: String): Pair { + 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) = 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) = 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 body } - fun changeQuotesVisibility(areVisible: Boolean) { - _areQuotesVisible.value = areVisible + fun changeQuotesButtonVisibility(isVisible: Boolean) { + _isQuotesButtonVisible.value = isVisible } private suspend fun getLocalOrRemoteDraft(localUuid: String?): Draft? { @@ -979,7 +1011,7 @@ class NewMessageViewModel @Inject constructor( ) subject = subjectValue - body = sanitizeBody(uiBodyValue) + body = removeEmptyQuotes(uiBodyValue) /** * If we are opening for the 1st time an existing Draft created somewhere else @@ -1000,23 +1032,23 @@ class NewMessageViewModel @Inject constructor( } } - private fun sanitizeBody(html: String): String { + private fun removeEmptyQuotes(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. - if (bodyHasQuotes(html)) { + // If the user deleted the quotes text, remove the quotes div so the button to show quotes doesn't show. + if (bodyHasEmptyQuotes(html)) { doc.getElementsByClass(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME).forEach { it.remove() } } - return doc.html() + return doc.body().html() } - fun bodyHasQuotes(body: String): Boolean { + private fun bodyHasEmptyQuotes(body: String): Boolean { val replyQuotes = JsoupParserUtil.jsoupParseBodyFragmentWithLog(body) .getElementsByClass(MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME) val forwardQuotes = JsoupParserUtil.jsoupParseBodyFragmentWithLog(body) .getElementsByClass(MessageBodyUtils.INFOMANIAK_FORWARD_QUOTE_HTML_CLASS_NAME) - return replyQuotes.isNotEmpty() && replyQuotes.hasText() || forwardQuotes.isNotEmpty() && forwardQuotes.hasText() + return replyQuotes.isNotEmpty() && !replyQuotes.hasText() || forwardQuotes.isNotEmpty() && !forwardQuotes.hasText() } private fun Draft.updateDraftAttachmentsWithLiveData(uiAttachments: List, step: String) { @@ -1058,15 +1090,12 @@ class NewMessageViewModel @Inject constructor( private fun isSnapshotTheSame(subjectValue: String?, uiBodyValue: String): Boolean { return snapshot?.let { draftSnapshot -> - draftSnapshot.identityId == fromLiveData.value?.signature?.id?.toString() && - draftSnapshot.to == toLiveData.valueOrEmpty().toSet() && - draftSnapshot.cc == ccLiveData.valueOrEmpty().toSet() && - draftSnapshot.bcc == bccLiveData.valueOrEmpty().toSet() && - draftSnapshot.subject == subjectValue && - sanitizeBody(draftSnapshot.uiBody) == sanitizeBody(uiBodyValue) && - draftSnapshot.isEncrypted == isEncryptionActivated.value && - draftSnapshot.encryptionPassword == encryptionPassword.value && - draftSnapshot.attachmentsLocalUuids == attachmentsLiveData.valueOrEmpty() + Log.d("DRAFT BODY", draftSnapshot.uiBody) + Log.d("BODY", uiBodyValue) + draftSnapshot.identityId == fromLiveData.value?.signature?.id?.toString() && draftSnapshot.to == toLiveData.valueOrEmpty() + .toSet() && draftSnapshot.cc == ccLiveData.valueOrEmpty() + .toSet() && draftSnapshot.bcc == bccLiveData.valueOrEmpty() + .toSet() && draftSnapshot.subject == subjectValue && draftSnapshot.uiBody == uiBodyValue && draftSnapshot.isEncrypted == isEncryptionActivated.value && draftSnapshot.encryptionPassword == encryptionPassword.value && draftSnapshot.attachmentsLocalUuids == attachmentsLiveData.valueOrEmpty() .mapTo(mutableSetOf()) { it.localUuid } } ?: false } diff --git a/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt b/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt index 6166b817862..cffe9dbd980 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt @@ -27,7 +27,6 @@ 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 @@ -211,11 +210,6 @@ 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) @@ -230,8 +224,6 @@ class HtmlFormatter(private val html: String) { listOf(PRIMARY_COLOR_CODE to getAttributeColor(RAndroid.attr.colorPrimary)) ) - fun Context.getHideQuotesStyle(): String = loadCss(R.raw.hide_quotes_style) - fun Context.getPrintMailStyle(): String = loadCss(R.raw.print_email) fun Context.getResizeScript(): String = loadScript( @@ -239,18 +231,13 @@ class HtmlFormatter(private val html: String) { listOf("MESSAGE_SELECTOR" to "#$KMAIL_MESSAGE_ID") ) - private var cachedToggleQuotesScript: String? = null - fun Context.getToggleQuotesButtonVisibilityScript(): String { - return cachedToggleQuotesScript ?: loadScript(R.raw.show_quotes_script).also { cachedToggleQuotesScript = it } - } + fun Context.getHideQuotesStyle(): String = loadCss(R.raw.hide_quotes_style) + + fun Context.getShowQuotesScript(): String = loadScript(R.raw.show_quotes_script) fun Context.getAddStyleWithIdScript(): String = loadScript(R.raw.add_style_with_id_script) - private var cachedReplaceSignatureScript: String? = null - fun Context.getReplaceSignatureScript(): String { - return cachedReplaceSignatureScript - ?: loadScript(R.raw.replace_signature_script).also { cachedReplaceSignatureScript = it } - } + fun Context.getReplaceSignatureScript(): String = loadScript(R.raw.replace_signature_script) fun Context.getFixStyleScript(): String { return loadScript(R.raw.fix_email_style) diff --git a/app/src/main/res/layout/fragment_new_message.xml b/app/src/main/res/layout/fragment_new_message.xml index 92131f84f97..66208c23dc1 100644 --- a/app/src/main/res/layout/fragment_new_message.xml +++ b/app/src/main/res/layout/fragment_new_message.xml @@ -320,11 +320,9 @@ diff --git a/app/src/main/res/raw/editor_style.css b/app/src/main/res/raw/editor_style.css index 212270a6280..0540cd3066a 100644 --- a/app/src/main/res/raw/editor_style.css +++ b/app/src/main/res/raw/editor_style.css @@ -5,4 +5,5 @@ html { body { margin-top: 0px; margin-bottom: 1rem; + overflow-x: auto !important; } diff --git a/app/src/main/res/raw/hide_quotes_style.css b/app/src/main/res/raw/hide_quotes_style.css index 7537ab08f78..80ee879896f 100644 --- a/app/src/main/res/raw/hide_quotes_style.css +++ b/app/src/main/res/raw/hide_quotes_style.css @@ -1,4 +1,3 @@ .ik_mail_quote, .forwardContentMessage { display: none; - overflow-x: auto !important; } diff --git a/app/src/main/res/raw/replace_signature_script.js b/app/src/main/res/raw/replace_signature_script.js index c1da757990a..d93c141fe97 100644 --- a/app/src/main/res/raw/replace_signature_script.js +++ b/app/src/main/res/raw/replace_signature_script.js @@ -17,16 +17,16 @@ */ (function() { - var signatureElement = document.querySelector('.%s'); - var newSigHtml = "%s"; + const signatureElement = document.querySelector('.%s'); + const newSigHtml = %s; if (signatureElement) { if (newSigHtml === "") { signatureElement.remove(); } else { - signatureElement.outerHTML = newSigHtml; + signatureElement.innerHTML = newSigHtml; } } else if (newSigHtml !== "") { - var quotes = document.querySelector('.%s'); + const quotes = document.querySelector('.%s'); if (quotes) { quotes.insertAdjacentHTML('beforebegin', newSigHtml); } else { diff --git a/app/src/main/res/raw/show_quotes_script.js b/app/src/main/res/raw/show_quotes_script.js index 8d4350719ab..fffa5e5e3c6 100644 --- a/app/src/main/res/raw/show_quotes_script.js +++ b/app/src/main/res/raw/show_quotes_script.js @@ -17,7 +17,7 @@ */ (function() { - var style = document.getElementById("quote-visibility") + const style = document.getElementById("quote-visibility") if (style) { style.remove() diff --git a/app/src/main/res/raw/style.css b/app/src/main/res/raw/style.css index 70880b5bbf2..c7eb27097b6 100644 --- a/app/src/main/res/raw/style.css +++ b/app/src/main/res/raw/style.css @@ -11,20 +11,6 @@ body { min-width: auto !important; } -#ik-quotes, #ik-quotes > * { - max-width: 100% !important; - box-sizing: border-box !important; -} - -#ik-quotes table { - width: auto !important; - max-width: 100% !important; -} - -img { - max-width: 100% !important; -} - blockquote { padding: 0.2em 1.2em !important; margin: 0 !important; diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f84fae5148c..63441a1d1b9 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -146,6 +146,16 @@ @dimen/marginStandardSmall + + -