From 4e3b5bb3014225f8521bfaf0ca3ff1c3fb2cf899 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Mon, 1 Jun 2026 16:27:28 +0200 Subject: [PATCH 1/7] fix: Removed unused strings --- app/src/main/res/values-da/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-el/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-fi/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-nb/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 1 - app/src/main/res/values-pl/strings.xml | 1 - app/src/main/res/values-pt/strings.xml | 1 - app/src/main/res/values-sv/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - 13 files changed, 13 deletions(-) diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 712ad17a51..278ad28fe9 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -22,7 +22,6 @@ Arkivér Bloker afsender Annuller udsættelse - Luk resumévinduet Slet Rediger menu Videresend diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c5b6bbf4d3..50eb46ca87 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -22,7 +22,6 @@ Archiv Absender blockieren Schlummern abbrechen - Zusammenfassungsfenster schliessen Löschen Menü Bearbeiten Weiterleiten diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index a9bf5c856d..acce9a1ab0 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -22,7 +22,6 @@ Αρχειοθέτηση Αποκλεισμός αποστολέα Ακύρωση αναβολής - Κλείσιμο του παραθύρου σύνοψης Διαγραφή Επεξεργασία μενού Προώθηση diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 56a93ae08c..6f49e685e5 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -22,7 +22,6 @@ Archivar Bloquear remitente Cancelar el aplazamiento - Cerrar la ventana de resumen Borrar Menú Edición Reenviar diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index be4143ca75..b4f73e943d 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -22,7 +22,6 @@ Arkistoi Estä lähettäjä Peruuta torkku - Sulje yhteenvetoikkuna Poista Muokkaa valikkoa Välitä diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0086481de6..d8c49bb32f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -22,7 +22,6 @@ Archiver Bloquer l’expéditeur Annuler la mise en attente - Fermer la fenêtre de résumé Supprimer Modifier le menu Transférer diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 6c661cb56f..b2970b5d7b 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -22,7 +22,6 @@ Archivia Blocca mittente Annulla il posticipo - Chiudi la finestra di riepilogo Cestina Menu Modifica Inoltra diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 5acd244a1d..9ce21d72a0 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -22,7 +22,6 @@ Arkiver Blokker avsender Avbryt utsettelse - Lukk oppsummeringsvinduet Slett Rediger meny Videresend diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 9f6da6f0cf..3815a14e85 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -22,7 +22,6 @@ Archiveren Afzender blokkeren Snooze annuleren - Samenvattingsvenster sluiten Verwijderen Menu bewerken Doorsturen diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a9af0a5637..ec6df962f6 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -22,7 +22,6 @@ Archiwizuj Zablokuj nadawcę Anuluj uśpienie - Zamknij okno podsumowania Usuń Edytuj menu Prześlij dalej diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index ea4d6de18b..05f1a35771 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -22,7 +22,6 @@ Arquivar Bloquear remetente Cancelar lembrete - Fechar a janela de resumo Eliminar Editar menu Encaminhar diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 570fc12ce5..7ab3a6c24e 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -22,7 +22,6 @@ Arkivera Blockera avsändare Avbryt vänteläge - Stäng sammanfattningsfönstret Ta bort Redigera meny Vidarebefordra diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2c80966418..44442b1d0e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -26,7 +26,6 @@ Archive Block sender Cancel snooze - Close summary window Delete Edit menu Forward From b642717abb5e8d5e004fe4eb78a8b35c06fd3bc6 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Mon, 1 Jun 2026 17:01:26 +0200 Subject: [PATCH 2/7] fix: Update Matomo name --- app/src/main/java/com/infomaniak/mail/MatomoMail.kt | 2 +- .../ui/main/thread/actions/MessageActionsBottomSheetDialog.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/MatomoMail.kt b/app/src/main/java/com/infomaniak/mail/MatomoMail.kt index 0a5883f7ed..1e6a153226 100644 --- a/app/src/main/java/com/infomaniak/mail/MatomoMail.kt +++ b/app/src/main/java/com/infomaniak/mail/MatomoMail.kt @@ -321,7 +321,7 @@ object MatomoMail : Matomo { SpamFolder("spamFolder"), SpamSwipe("spamSwipe"), StrikeThrough("strikeThrough"), - Summary("summary"), + Summarize("summarize"), Translate("translate"), Switch("switch"), SwitchAccount("switchAccount"), diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MessageActionsBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MessageActionsBottomSheetDialog.kt index a00bccd7c7..c9c4bdd5d5 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MessageActionsBottomSheetDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MessageActionsBottomSheetDialog.kt @@ -288,7 +288,7 @@ class MessageActionsBottomSheetDialog : MailActionsBottomSheetDialog() { } override fun onSummary() { - trackBottomSheetThreadActionsEvent(MatomoName.Summary) + trackBottomSheetThreadActionsEvent(MatomoName.Summarize) setBackNavigationResult(OPEN_AI_SUMMARY_BOTTOM_SHEET, message.uid) } From 14b90694f79129885587895b33159db13fa0f38e Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Mon, 1 Jun 2026 17:12:14 +0200 Subject: [PATCH 3/7] fix: Add copyright --- .../main/res/drawable/ic_paragraph_shorten.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/main/res/drawable/ic_paragraph_shorten.xml b/app/src/main/res/drawable/ic_paragraph_shorten.xml index 494d152793..e3c4c43ff6 100644 --- a/app/src/main/res/drawable/ic_paragraph_shorten.xml +++ b/app/src/main/res/drawable/ic_paragraph_shorten.xml @@ -1,3 +1,20 @@ + Date: Tue, 2 Jun 2026 09:00:04 +0200 Subject: [PATCH 4/7] refactor: Clean code --- .../mail/ui/main/thread/ThreadAdapter.kt | 3 +-- .../mail/ui/main/thread/ThreadFragment.kt | 18 +++++++++--------- .../mail/ui/main/thread/ThreadState.kt | 3 ++- .../actions/MailActionsBottomSheetDialog.kt | 12 ++++++------ .../{LanguageExt.kt => ContextExt.kt} | 3 +-- 5 files changed, 19 insertions(+), 20 deletions(-) rename app/src/main/java/com/infomaniak/mail/utils/extensions/{LanguageExt.kt => ContextExt.kt} (94%) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt index 66fe31c435..41fdc43724 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt @@ -99,7 +99,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.time.format.FormatStyle import java.util.Date -import kotlin.context import androidx.appcompat.R as RAndroid import com.google.android.material.R as RMaterial @@ -620,7 +619,7 @@ class ThreadAdapter( isButtonEnabled = true hideButtonProgress(R.string.aiButtonRetry) - if (state.isRetry && !state.wasLoaderShown) { + if (state.isRetryAttempt && !state.wasLoaderShown) { threadAdapterCallbacks?.showSnackbarRetry?.invoke(errorMessageRes) } } else { diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt index 568f62b701..ddea83b850 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt @@ -1065,17 +1065,17 @@ class ThreadFragment : Fragment(), PickerEmojiObserver { } private fun doAiAction(messageUid: String, aiAction: AiAction) { - val isRetry = getStateMap(aiAction)[messageUid] is AiProcessState.Error + val isRetryAttempt = getStateMap(aiAction)[messageUid] is AiProcessState.Error cancelRetryTimer(messageUid, aiAction) - val initialState = if (isRetry) AiProcessState.Retrying(isLoaderVisible = false) else AiProcessState.Loading + val initialState = if (isRetryAttempt) AiProcessState.Retrying(isLoaderVisible = false) else AiProcessState.Loading updateAiProcessState(messageUid, aiAction, initialState) - startRetryTimerIfNeeded(messageUid, aiAction, isRetry) + startRetryTimerIfNeeded(messageUid, aiAction, isRetryAttempt) viewLifecycleOwner.lifecycleScope.launch { - processAiApiCall(messageUid, aiAction, isRetry) + processAiApiCall(messageUid, aiAction, isRetryAttempt) } } @@ -1181,13 +1181,13 @@ class ThreadFragment : Fragment(), PickerEmojiObserver { private fun mapApiResultToState( result: ApiResponse?, - isRetry: Boolean, + isRetryAttempt: Boolean, wasLoaderShown: Boolean ): AiProcessState { if (result == null) { return AiProcessState.Error( canRetry = true, - isRetry = isRetry, + isRetryAttempt = isRetryAttempt, wasLoaderShown = wasLoaderShown, targetSameAsSource = false, ) @@ -1200,7 +1200,7 @@ class ThreadFragment : Fragment(), PickerEmojiObserver { val targetSameAsSource = result.error?.code == ErrorCode.TRANSLATION_TARGET_SAME_AS_SOURCE return AiProcessState.Error( canRetry = canRetry, - isRetry = isRetry, + isRetryAttempt = isRetryAttempt, wasLoaderShown = wasLoaderShown, targetSameAsSource = targetSameAsSource, ) @@ -1224,8 +1224,8 @@ class ThreadFragment : Fragment(), PickerEmojiObserver { timersMap.remove(messageUid) } - private fun startRetryTimerIfNeeded(messageUid: String, aiAction: AiAction, isRetry: Boolean) { - if (!isRetry) return + private fun startRetryTimerIfNeeded(messageUid: String, aiAction: AiAction, isRetryAttempt: Boolean) { + if (!isRetryAttempt) return val timer = UtilsLegacy.createRefreshTimer { val state = getStateMap(aiAction)[messageUid] diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadState.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadState.kt index 8955ee6df9..ffb2b2a200 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadState.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadState.kt @@ -74,9 +74,10 @@ sealed class AiProcessState { data class Success(val content: String = "") : AiProcessState() data class Error( val canRetry: Boolean, - val isRetry: Boolean = true, + val isRetryAttempt: Boolean = true, val wasLoaderShown: Boolean = false, val targetSameAsSource: Boolean = false ) : AiProcessState() + data object Dismissed : AiProcessState() } diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MailActionsBottomSheetDialog.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MailActionsBottomSheetDialog.kt index 98b7b804e5..c3c6211c2a 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MailActionsBottomSheetDialog.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/MailActionsBottomSheetDialog.kt @@ -187,14 +187,14 @@ abstract class MailActionsBottomSheetDialog : ActionsBottomSheetDialog() { isVisible = canBeReactedTo } - fun setTranslateUi(isEnabled: Boolean) = with(binding.translate) { - this.isEnabled = isEnabled - isVisible = true + fun setTranslateUi(isEnabled: Boolean) { + binding.translate.isEnabled = isEnabled + binding.translate.isVisible = true } - fun setSummaryUi(isEnabled: Boolean) = with(binding.summary) { - this.isEnabled = isEnabled - isVisible = true + fun setSummaryUi(isEnabled: Boolean) { + binding.summary.isEnabled = isEnabled + binding.summary.isVisible = true } interface OnActionClick { diff --git a/app/src/main/java/com/infomaniak/mail/utils/extensions/LanguageExt.kt b/app/src/main/java/com/infomaniak/mail/utils/extensions/ContextExt.kt similarity index 94% rename from app/src/main/java/com/infomaniak/mail/utils/extensions/LanguageExt.kt rename to app/src/main/java/com/infomaniak/mail/utils/extensions/ContextExt.kt index f3b4b25c6c..190fca9501 100644 --- a/app/src/main/java/com/infomaniak/mail/utils/extensions/LanguageExt.kt +++ b/app/src/main/java/com/infomaniak/mail/utils/extensions/ContextExt.kt @@ -30,8 +30,7 @@ fun Context.getCurrentLanguageCode(): String { resources.configuration.locales[0]?.language } - val cleanCode = languageCode?.lowercase()?.take(2) ?: defaultLanguage + val cleanCode = languageCode?.take(2)?.lowercase() ?: defaultLanguage return cleanCode } - From a36cbf557a272da982f23659ce16f751122edc9f Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Tue, 2 Jun 2026 11:45:19 +0200 Subject: [PATCH 5/7] refactor: Move logic from Fragment to ViewModel --- .../mail/ui/main/thread/ThreadAdapter.kt | 4 +- .../mail/ui/main/thread/ThreadFragment.kt | 208 +++--------------- .../mail/ui/main/thread/ThreadViewModel.kt | 193 ++++++++++++++++ 3 files changed, 220 insertions(+), 185 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt index 41fdc43724..4ab300200d 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt @@ -63,7 +63,7 @@ import com.infomaniak.mail.data.models.message.Message import com.infomaniak.mail.databinding.ItemMessageBinding import com.infomaniak.mail.databinding.ItemSuperCollapsedBlockBinding import com.infomaniak.mail.ui.main.thread.ThreadAdapter.ThreadAdapterViewHolder -import com.infomaniak.mail.ui.main.thread.ThreadFragment.AiAction +import com.infomaniak.mail.ui.main.thread.ThreadViewModel.AiAction import com.infomaniak.mail.ui.main.thread.models.MessageUi import com.infomaniak.mail.ui.main.thread.models.MessageUi.UnsubscribeState import com.infomaniak.mail.ui.main.thread.webViewClient.MessageDisplayWebViewClient @@ -479,7 +479,7 @@ class ThreadAdapter( AiAction.TRANSLATE -> threadAdapterState.aiTranslateStateMap[messageUid] } - private fun MessageViewHolder.bindAiAction(message: Message, aiAction: ThreadFragment.AiAction? = null) { + private fun MessageViewHolder.bindAiAction(message: Message, aiAction: AiAction? = null) { if (aiAction == null) { bindAiAction(message, AiAction.SUMMARY) bindAiAction(message, AiAction.TRANSLATE) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt index ddea83b850..8c85614db6 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadFragment.kt @@ -22,7 +22,6 @@ import android.content.res.Configuration import android.graphics.drawable.InsetDrawable import android.os.Build.VERSION.SDK_INT import android.os.Bundle -import android.os.CountDownTimer import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.View @@ -45,8 +44,6 @@ import com.infomaniak.core.ksuite.data.KSuite import com.infomaniak.core.legacy.utils.context import com.infomaniak.core.legacy.utils.getBackNavigationResult import com.infomaniak.core.legacy.views.DividerItemDecorator -import com.infomaniak.core.network.models.ApiResponse -import com.infomaniak.core.network.models.ApiResponseStatus import com.infomaniak.core.sentry.SentryLog import com.infomaniak.mail.MatomoMail.MatomoName import com.infomaniak.mail.MatomoMail.trackAttachmentActionsEvent @@ -61,7 +58,6 @@ import com.infomaniak.mail.R import com.infomaniak.mail.data.LocalSettings import com.infomaniak.mail.data.LocalSettings.AutoAdvanceMode import com.infomaniak.mail.data.LocalSettings.ExternalContent -import com.infomaniak.mail.data.api.ApiRepository import com.infomaniak.mail.data.api.ApiRoutes import com.infomaniak.mail.data.models.Attachable import com.infomaniak.mail.data.models.Attachment @@ -70,7 +66,6 @@ import com.infomaniak.mail.data.models.SwissTransferFile import com.infomaniak.mail.data.models.calendar.Attendee import com.infomaniak.mail.data.models.correspondent.Recipient import com.infomaniak.mail.data.models.draft.Draft.DraftMode -import com.infomaniak.mail.data.models.message.Body import com.infomaniak.mail.data.models.message.Message import com.infomaniak.mail.data.models.snooze.BatchSnoozeResult import com.infomaniak.mail.data.models.thread.Thread @@ -100,6 +95,8 @@ import com.infomaniak.mail.ui.main.thread.SubjectFormatter.SubjectData import com.infomaniak.mail.ui.main.thread.ThreadAdapter.ContextMenuType import com.infomaniak.mail.ui.main.thread.ThreadAdapter.DisplayType import com.infomaniak.mail.ui.main.thread.ThreadAdapter.ThreadAdapterCallbacks +import com.infomaniak.mail.ui.main.thread.ThreadViewModel.AiAction +import com.infomaniak.mail.ui.main.thread.ThreadViewModel.AiBodyUpdate import com.infomaniak.mail.ui.main.thread.ThreadViewModel.SnoozeScheduleType import com.infomaniak.mail.ui.main.thread.ThreadViewModel.ThreadHeaderVisibility import com.infomaniak.mail.ui.main.thread.actions.AttachmentActionsBottomSheetDialogArgs @@ -112,14 +109,11 @@ import com.infomaniak.mail.ui.main.thread.actions.ThreadActionsBottomSheetDialog import com.infomaniak.mail.ui.main.thread.calendar.AttendeesBottomSheetDialogArgs import com.infomaniak.mail.ui.main.thread.encryption.UnencryptableRecipientsBottomSheetDialogArgs import com.infomaniak.mail.ui.main.thread.models.MessageUi -import com.infomaniak.mail.utils.ErrorCode import com.infomaniak.mail.utils.FolderRoleUtils -import com.infomaniak.mail.utils.MessageBodyUtils import com.infomaniak.mail.utils.PermissionUtils import com.infomaniak.mail.utils.SharedUtils import com.infomaniak.mail.utils.UiUtils import com.infomaniak.mail.utils.UiUtils.dividerDrawable -import com.infomaniak.mail.utils.Utils import com.infomaniak.mail.utils.Utils.runCatchingRealm import com.infomaniak.mail.utils.WorkerUtils import com.infomaniak.mail.utils.date.MailDateFormatUtils.formatDayOfWeekAdaptiveYear @@ -133,7 +127,6 @@ import com.infomaniak.mail.utils.extensions.changeToolbarColorOnScroll import com.infomaniak.mail.utils.extensions.copyStringToClipboard import com.infomaniak.mail.utils.extensions.deleteWithConfirmationPopup import com.infomaniak.mail.utils.extensions.getAttributeColor -import com.infomaniak.mail.utils.extensions.getCurrentLanguageCode import com.infomaniak.mail.utils.extensions.isTabletOrFoldable import com.infomaniak.mail.utils.extensions.navigateToDownloadProgressDialog import com.infomaniak.mail.utils.extensions.observeNotNull @@ -155,7 +148,6 @@ import kotlin.math.absoluteValue import kotlin.math.min import kotlin.math.roundToInt import androidx.appcompat.R as RAndroid -import com.infomaniak.core.legacy.utils.Utils as UtilsLegacy @AndroidEntryPoint class ThreadFragment : Fragment(), PickerEmojiObserver { @@ -213,9 +205,6 @@ class ThreadFragment : Fragment(), PickerEmojiObserver { private val twoPaneViewModel: TwoPaneViewModel by activityViewModels() private val threadViewModel: ThreadViewModel by viewModels() - private val aiSummaryRetryTimers = mutableMapOf() - private val aiTranslateRetryTimers = mutableMapOf() - private val twoPaneFragment inline get() = parentFragment as TwoPaneFragment private val threadAdapter inline get() = binding.messagesList.adapter as ThreadAdapter private val isNotInSpam: Boolean @@ -259,6 +248,8 @@ class ThreadFragment : Fragment(), PickerEmojiObserver { observeMessageOfUserToBlock() observeCanSendEmails() + + observeAiStateUpdates() } private fun handleEdgeToEdge() = with(binding) { @@ -296,8 +287,6 @@ class ThreadFragment : Fragment(), PickerEmojiObserver { } override fun onDestroyView() { - aiSummaryRetryTimers.values.forEach { it.cancel() } - aiSummaryRetryTimers.clear() threadAdapter.resetCallbacks() super.onDestroyView() @@ -463,10 +452,10 @@ class ThreadFragment : Fragment(), PickerEmojiObserver { binding.emojiReactionDetailsBottomSheet.showBottomSheetFor(emojiDetails, preselectedEmojiTab = emoji) } }, - onAiBannerRetry = { messageUid, aiAction -> doAiAction(messageUid, aiAction) }, + onAiBannerRetry = { messageUid, aiAction -> threadViewModel.doAiAction(messageUid, aiAction) }, showSnackbarRetry = { errorMessage -> snackbarManager.setValue(getString(errorMessage)) }, - onAiBannerClose = { messageUid, aiAction -> dismissAiAction(messageUid, aiAction) }, - onShowOriginal = { messageUid -> dismissAiAction(messageUid, AiAction.TRANSLATE) }, + onAiBannerClose = { messageUid, aiAction -> threadViewModel.dismissAiAction(messageUid, aiAction) }, + onShowOriginal = { messageUid -> threadViewModel.dismissAiAction(messageUid, AiAction.TRANSLATE) }, ), ) @@ -836,11 +825,11 @@ class ThreadFragment : Fragment(), PickerEmojiObserver { } getBackNavigationResult(OPEN_AI_SUMMARY_BOTTOM_SHEET) { messageUid: String -> - doAiAction(messageUid, AiAction.SUMMARY) + threadViewModel.doAiAction(messageUid, AiAction.SUMMARY) } getBackNavigationResult(OPEN_AI_TRANSLATE_BOTTOM_SHEET) { messageUid: String -> - doAiAction(messageUid, AiAction.TRANSLATE) + threadViewModel.doAiAction(messageUid, AiAction.TRANSLATE) } } @@ -1054,29 +1043,26 @@ class ThreadFragment : Fragment(), PickerEmojiObserver { ) } - private fun getStateMap(action: AiAction) = when (action) { - AiAction.SUMMARY -> threadViewModel.threadState.aiSummaryStateMap - AiAction.TRANSLATE -> threadViewModel.threadState.aiTranslateStateMap - } - - private fun getTimerMap(action: AiAction) = when (action) { - AiAction.SUMMARY -> aiSummaryRetryTimers - AiAction.TRANSLATE -> aiTranslateRetryTimers + private fun observeAiStateUpdates() { + threadViewModel.aiStateUpdates.observe(viewLifecycleOwner) { (messageUid, aiAction, bodyUpdate) -> + when (bodyUpdate) { + AiBodyUpdate.SHOW_TRANSLATED -> reloadMessageInAdapter(messageUid) + AiBodyUpdate.SHOW_ORIGINAL -> showOriginalMessage(messageUid) + AiBodyUpdate.NONE -> Unit + } + notifyAiStateChanged(messageUid, aiAction) + } } - private fun doAiAction(messageUid: String, aiAction: AiAction) { - val isRetryAttempt = getStateMap(aiAction)[messageUid] is AiProcessState.Error - - cancelRetryTimer(messageUid, aiAction) - - val initialState = if (isRetryAttempt) AiProcessState.Retrying(isLoaderVisible = false) else AiProcessState.Loading - updateAiProcessState(messageUid, aiAction, initialState) - - startRetryTimerIfNeeded(messageUid, aiAction, isRetryAttempt) + private fun notifyAiStateChanged(messageUid: String, aiAction: AiAction) { + val index = threadAdapter.currentList.indexOfFirst { it is MessageUi && it.message.uid == messageUid } + if (index < 0) return - viewLifecycleOwner.lifecycleScope.launch { - processAiApiCall(messageUid, aiAction, isRetryAttempt) + val notifyType = when (aiAction) { + AiAction.SUMMARY -> ThreadAdapter.NotifyType.AiSummaryStateChanged + AiAction.TRANSLATE -> ThreadAdapter.NotifyType.AiTranslateStateChanged } + threadAdapter.notifyItemChanged(index, notifyType) } private fun showOriginalMessage(messageUid: String) { @@ -1090,83 +1076,6 @@ class ThreadFragment : Fragment(), PickerEmojiObserver { reloadMessageInAdapter(messageUid) } - private fun dismissAiAction(messageUid: String, aiAction: AiAction) { - cancelRetryTimer(messageUid, aiAction) - getStateMap(aiAction)[messageUid] = AiProcessState.Dismissed - - when (aiAction) { - AiAction.SUMMARY -> threadViewModel.removeSummary(messageUid) - AiAction.TRANSLATE -> { - threadViewModel.removeTranslation(messageUid) - showOriginalMessage(messageUid) - } - } - - val index = threadAdapter.currentList.indexOfFirst { it is MessageUi && it.message.uid == messageUid } - if (index < 0) return - - val notifyType = if (aiAction == AiAction.SUMMARY) { - ThreadAdapter.NotifyType.AiSummaryStateChanged - } else { - ThreadAdapter.NotifyType.AiTranslateStateChanged - } - threadAdapter.notifyItemChanged(index, notifyType) - } - - private suspend fun processAiApiCall(messageUid: String, aiAction: AiAction, isRetry: Boolean) { - val content = getMessageContent(messageUid) - val languageCode = requireContext().getCurrentLanguageCode() - - val result = when (aiAction) { - AiAction.SUMMARY -> ApiRepository.aiSummary(languageCode, content) - AiAction.TRANSLATE -> { - val mailbox = mainViewModel.currentMailbox.value ?: mainViewModel.openMailbox() - mailbox?.let { ApiRepository.aiTranslate(languageCode, it.uuid, messageUid) } - } - } - - cancelRetryTimer(messageUid, aiAction) - - val currentState = getStateMap(aiAction)[messageUid] - if (currentState is AiProcessState.Dismissed) return - - val wasLoaderShown = currentState is AiProcessState.Retrying && currentState.isLoaderVisible - - val finalState = mapApiResultToState(result, isRetry, wasLoaderShown) - if (finalState is AiProcessState.Success && result != null) { - when (aiAction) { - AiAction.TRANSLATE -> handleTranslateSuccess(messageUid, result.data) - AiAction.SUMMARY -> handleSummarySuccess(messageUid, result.data) - } - } - updateAiProcessState(messageUid, aiAction, finalState) - } - - private suspend fun handleTranslateSuccess(messageUid: String, data: String?) { - val translatedHtml = data ?: "" - threadViewModel.saveTranslation(messageUid, translatedHtml) - - val message = mainViewModel.getMessage(messageUid) - val bodyType = message?.body?.type ?: Utils.TEXT_HTML - - val translatedBody = Body().apply { - value = translatedHtml - type = bodyType - } - - val translatedSplitBody = MessageBodyUtils.splitContentAndQuote(translatedBody) - threadViewModel.threadState.cachedTranslatedSplitBodies[messageUid] = translatedSplitBody - - message?.splitBody = translatedSplitBody - reloadMessageInAdapter(messageUid) - } - - private fun handleSummarySuccess(messageUid: String, summaryContent: String?) { - if (summaryContent != null) { - threadViewModel.saveSummary(messageUid, summaryContent) - } - } - private fun reloadMessageInAdapter(messageUid: String) { val index = threadAdapter.currentList.indexOfFirst { it is MessageUi && it.message.uid == messageUid } if (index >= 0) { @@ -1174,69 +1083,6 @@ class ThreadFragment : Fragment(), PickerEmojiObserver { } } - private suspend fun getMessageContent(messageUid: String): String { - val message = mainViewModel.getMessage(messageUid) - return message?.body?.value ?: message?.splitBody?.content ?: "" - } - - private fun mapApiResultToState( - result: ApiResponse?, - isRetryAttempt: Boolean, - wasLoaderShown: Boolean - ): AiProcessState { - if (result == null) { - return AiProcessState.Error( - canRetry = true, - isRetryAttempt = isRetryAttempt, - wasLoaderShown = wasLoaderShown, - targetSameAsSource = false, - ) - } - if (result.result == ApiResponseStatus.SUCCESS) { - return AiProcessState.Success(result.data ?: "") - } - - val canRetry = result.error?.code == ErrorCode.TRANSLATION_API_NOT_AVAILABLE - val targetSameAsSource = result.error?.code == ErrorCode.TRANSLATION_TARGET_SAME_AS_SOURCE - return AiProcessState.Error( - canRetry = canRetry, - isRetryAttempt = isRetryAttempt, - wasLoaderShown = wasLoaderShown, - targetSameAsSource = targetSameAsSource, - ) - } - - private fun updateAiProcessState(messageUid: String, aiAction: AiAction, newState: AiProcessState) { - getStateMap(aiAction)[messageUid] = newState - val index = threadAdapter.currentList.indexOfFirst { it is MessageUi && it.message.uid == messageUid } - if (index < 0) return - - val notifyType = when (aiAction) { - AiAction.SUMMARY -> ThreadAdapter.NotifyType.AiSummaryStateChanged - AiAction.TRANSLATE -> ThreadAdapter.NotifyType.AiTranslateStateChanged - } - threadAdapter.notifyItemChanged(index, notifyType) - } - - private fun cancelRetryTimer(messageUid: String, aiAction: AiAction) { - val timersMap = getTimerMap(aiAction) - timersMap[messageUid]?.cancel() - timersMap.remove(messageUid) - } - - private fun startRetryTimerIfNeeded(messageUid: String, aiAction: AiAction, isRetryAttempt: Boolean) { - if (!isRetryAttempt) return - - val timer = UtilsLegacy.createRefreshTimer { - val state = getStateMap(aiAction)[messageUid] - if (state is AiProcessState.Retrying) { - updateAiProcessState(messageUid, aiAction, AiProcessState.Retrying(isLoaderVisible = true)) - } - } - - getTimerMap(aiAction)[messageUid] = timer - timer.start() - } private fun rescheduleDraft(draftResource: String, currentScheduledEpochMillis: Long?) { mainViewModel.draftResource = draftResource @@ -1366,10 +1212,6 @@ class ThreadFragment : Fragment(), PickerEmojiObserver { NEXT_CHRONOLOGICAL_THREAD, } - enum class AiAction { - SUMMARY, - TRANSLATE, - } companion object { private val TAG = ThreadFragment::class.java.simpleName diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt index c19eab61b4..5b453da952 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt @@ -20,6 +20,7 @@ package com.infomaniak.mail.ui.main.thread import android.app.Application +import android.os.CountDownTimer import android.os.Parcelable import androidx.annotation.StringRes import androidx.lifecycle.AndroidViewModel @@ -31,6 +32,7 @@ import androidx.lifecycle.map import androidx.lifecycle.viewModelScope import com.infomaniak.core.legacy.utils.SingleLiveEvent import com.infomaniak.core.network.models.ApiResponse +import com.infomaniak.core.network.models.ApiResponseStatus import com.infomaniak.emojicomponents.data.ReactionDetail import com.infomaniak.mail.MatomoMail.MatomoName import com.infomaniak.mail.MatomoMail.trackUserInfo @@ -61,6 +63,7 @@ import com.infomaniak.mail.ui.main.thread.models.EmojiReactionStateUi import com.infomaniak.mail.ui.main.thread.models.MessageUi import com.infomaniak.mail.ui.main.thread.models.MessageUi.UnsubscribeState import com.infomaniak.mail.utils.AccountUtils +import com.infomaniak.mail.utils.ErrorCode import com.infomaniak.mail.utils.FeatureAvailability import com.infomaniak.mail.utils.FeatureAvailability.isSnoozeAvailable import com.infomaniak.mail.utils.MessageBodyUtils @@ -72,6 +75,7 @@ import com.infomaniak.mail.utils.coroutineContext import com.infomaniak.mail.utils.extensions.MergedContactDictionary import com.infomaniak.mail.utils.extensions.appContext import com.infomaniak.mail.utils.extensions.atLeastOneSucceeded +import com.infomaniak.mail.utils.extensions.getCurrentLanguageCode import com.infomaniak.mail.utils.extensions.getUids import com.infomaniak.mail.utils.extensions.indexOfFirstOrNull import com.infomaniak.mail.views.itemViews.AvatarMergedContactData @@ -111,6 +115,7 @@ import kotlinx.parcelize.Parcelize import splitties.coroutines.suspendLazy import splitties.experimental.ExperimentalSplittiesApi import javax.inject.Inject +import com.infomaniak.core.legacy.utils.Utils as UtilsLegacy /* Please note that, for the moment, the logic that uses this list assumes that the items are necessarily messages. If this were to change, it would be necessary to verify the types of the elements. */ @@ -220,6 +225,11 @@ class ThreadViewModel @Inject constructor( val quickActionBarClicks = SingleLiveEvent() + private val aiSummaryRetryTimers = mutableMapOf() + private val aiTranslateRetryTimers = mutableMapOf() + + val aiStateUpdates = SingleLiveEvent() + val failedMessagesUids = SingleLiveEvent>() val deletedMessagesUids = mutableSetOf() @@ -470,6 +480,172 @@ class ThreadViewModel @Inject constructor( threadState.cachedTranslatedSplitBodies.remove(messageUid) } + fun doAiAction(messageUid: String, aiAction: AiAction) { + val isRetryAttempt = getStateMap(aiAction)[messageUid] is AiProcessState.Error + + cancelRetryTimer(messageUid, aiAction) + + val initialState = if (isRetryAttempt) AiProcessState.Retrying(isLoaderVisible = false) else AiProcessState.Loading + updateAiProcessState(messageUid, aiAction, initialState) + + startRetryTimerIfNeeded(messageUid, aiAction, isRetryAttempt) + + viewModelScope.launch { processAiApiCall(messageUid, aiAction, isRetryAttempt) } + } + + fun dismissAiAction(messageUid: String, aiAction: AiAction) { + cancelRetryTimer(messageUid, aiAction) + + val bodyUpdate = when (aiAction) { + AiAction.SUMMARY -> { + removeSummary(messageUid) + AiBodyUpdate.NONE + } + AiAction.TRANSLATE -> { + removeTranslation(messageUid) + AiBodyUpdate.SHOW_ORIGINAL + } + } + + updateAiProcessState(messageUid, aiAction, AiProcessState.Dismissed, bodyUpdate) + } + + private suspend fun processAiApiCall(messageUid: String, aiAction: AiAction, isRetry: Boolean) { + val content = getMessageContent(messageUid) + val languageCode = appContext.getCurrentLanguageCode() + + val result = when (aiAction) { + AiAction.SUMMARY -> ApiRepository.aiSummary(languageCode, content) + AiAction.TRANSLATE -> ApiRepository.aiTranslate(languageCode, mailbox().uuid, messageUid) + } + + cancelRetryTimer(messageUid, aiAction) + + val currentState = getStateMap(aiAction)[messageUid] + if (currentState is AiProcessState.Dismissed) return + + val wasLoaderShown = currentState is AiProcessState.Retrying && currentState.isLoaderVisible + + val finalState = mapApiResultToState(result, isRetry, wasLoaderShown) + + var bodyUpdate = AiBodyUpdate.NONE + if (finalState is AiProcessState.Success) { + when (aiAction) { + AiAction.TRANSLATE -> { + handleTranslateSuccess(messageUid, result.data) + bodyUpdate = AiBodyUpdate.SHOW_TRANSLATED + } + AiAction.SUMMARY -> handleSummarySuccess(messageUid, result.data) + } + } + updateAiProcessState(messageUid, aiAction, finalState, bodyUpdate) + } + + private suspend fun handleTranslateSuccess(messageUid: String, data: String?) { + val translatedHtml = data ?: "" + saveTranslation(messageUid, translatedHtml) + + val message = messageController.getMessage(messageUid) + val bodyType = message?.body?.type ?: Utils.TEXT_HTML + + val translatedBody = Body().apply { + value = translatedHtml + type = bodyType + } + + val translatedSplitBody = MessageBodyUtils.splitContentAndQuote(translatedBody) + threadState.cachedTranslatedSplitBodies[messageUid] = translatedSplitBody + + message?.splitBody = translatedSplitBody + } + + private fun handleSummarySuccess(messageUid: String, summaryContent: String?) { + if (summaryContent != null) saveSummary(messageUid, summaryContent) + } + + private suspend fun getMessageContent(messageUid: String): String { + val message = messageController.getMessage(messageUid) + return message?.body?.value ?: message?.splitBody?.content ?: "" + } + + private fun mapApiResultToState( + result: ApiResponse?, + isRetryAttempt: Boolean, + wasLoaderShown: Boolean, + ): AiProcessState { + if (result == null) { + return AiProcessState.Error( + canRetry = true, + isRetryAttempt = isRetryAttempt, + wasLoaderShown = wasLoaderShown, + targetSameAsSource = false, + ) + } + if (result.result == ApiResponseStatus.SUCCESS) { + return AiProcessState.Success(result.data ?: "") + } + + val canRetry = result.error?.code == ErrorCode.TRANSLATION_API_NOT_AVAILABLE + val targetSameAsSource = result.error?.code == ErrorCode.TRANSLATION_TARGET_SAME_AS_SOURCE + return AiProcessState.Error( + canRetry = canRetry, + isRetryAttempt = isRetryAttempt, + wasLoaderShown = wasLoaderShown, + targetSameAsSource = targetSameAsSource, + ) + } + + private fun updateAiProcessState( + messageUid: String, + aiAction: AiAction, + newState: AiProcessState, + bodyUpdate: AiBodyUpdate = AiBodyUpdate.NONE, + ) { + getStateMap(aiAction)[messageUid] = newState + aiStateUpdates.postValue(AiStateUpdate(messageUid, aiAction, bodyUpdate)) + } + + private fun getStateMap(action: AiAction) = when (action) { + AiAction.SUMMARY -> threadState.aiSummaryStateMap + AiAction.TRANSLATE -> threadState.aiTranslateStateMap + } + + private fun getTimerMap(action: AiAction) = when (action) { + AiAction.SUMMARY -> aiSummaryRetryTimers + AiAction.TRANSLATE -> aiTranslateRetryTimers + } + + private fun cancelRetryTimer(messageUid: String, aiAction: AiAction) { + val timersMap = getTimerMap(aiAction) + timersMap[messageUid]?.cancel() + timersMap.remove(messageUid) + } + + private fun startRetryTimerIfNeeded(messageUid: String, aiAction: AiAction, isRetryAttempt: Boolean) { + if (!isRetryAttempt) return + + val timer = UtilsLegacy.createRefreshTimer { + val state = getStateMap(aiAction)[messageUid] + if (state is AiProcessState.Retrying) { + updateAiProcessState(messageUid, aiAction, AiProcessState.Retrying(isLoaderVisible = true)) + } + } + + getTimerMap(aiAction)[messageUid] = timer + timer.start() + } + + private fun cancelAllAiRetryTimers() { + (aiSummaryRetryTimers.values + aiTranslateRetryTimers.values).forEach { it.cancel() } + aiSummaryRetryTimers.clear() + aiTranslateRetryTimers.clear() + } + + override fun onCleared() { + cancelAllAiRetryTimers() + super.onCleared() + } + private fun markThreadAsSeen(thread: Thread) = viewModelScope.launch(ioCoroutineContext) { sharedUtils.markAsSeen(mailbox(), listOf(thread)) } @@ -795,6 +971,23 @@ class ThreadViewModel @Inject constructor( DISPLAYED, COLLAPSED, FIRST_AFTER_BLOCK, } + enum class AiAction { + SUMMARY, + TRANSLATE, + } + + data class AiStateUpdate( + val messageUid: String, + val aiAction: AiAction, + val bodyUpdate: AiBodyUpdate, + ) + + enum class AiBodyUpdate { + NONE, + SHOW_TRANSLATED, + SHOW_ORIGINAL, + } + sealed class SnoozeScheduleType(@StringRes val positiveButtonResId: Int) : Parcelable { abstract val threadUids: List From e16a3e858f3d3b7963d6bbdb165d6729c8e3f35e Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Wed, 3 Jun 2026 10:25:58 +0200 Subject: [PATCH 6/7] feat: Rename summarize route and use message ID instead of full body --- .../java/com/infomaniak/mail/data/api/ApiRepository.kt | 5 ++--- .../main/java/com/infomaniak/mail/data/api/ApiRoutes.kt | 2 +- .../com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt | 8 +------- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt b/app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt index 153b6cf7bc..52104bf75a 100644 --- a/app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt +++ b/app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt @@ -497,12 +497,11 @@ object ApiRepository : ApiRepositoryCore() { ) } - suspend fun aiSummary(languageCode: String, content: String): ApiResponse { - // TODO: Adapt aiSummary to accept an additional messageUid parameter once the backend supports it + suspend fun aiSummary(languageCode: String, mailboxUuid: String, msgUid: String): ApiResponse { return callApi( url = ApiRoutes.aiSummary(), method = POST, - body = mapOf("destination_language" to languageCode, "content" to content), + body = mapOf("destination_language" to languageCode, "mailbox_uuid" to mailboxUuid, "msg_uid" to msgUid), okHttpClient = HttpClient.okHttpClientLongTimeoutWithTokenInterceptor, ) } diff --git a/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt b/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt index 59ff2d08fc..3b625d902f 100644 --- a/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt +++ b/app/src/main/java/com/infomaniak/mail/data/api/ApiRoutes.kt @@ -272,7 +272,7 @@ object ApiRoutes { } fun aiSummary(): String { - return "$MAIL_API/api/resume" + return "$MAIL_API/api/summarize" } fun aiTranslate(): String { diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt index 5b453da952..4c9bdb5717 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt @@ -511,11 +511,10 @@ class ThreadViewModel @Inject constructor( } private suspend fun processAiApiCall(messageUid: String, aiAction: AiAction, isRetry: Boolean) { - val content = getMessageContent(messageUid) val languageCode = appContext.getCurrentLanguageCode() val result = when (aiAction) { - AiAction.SUMMARY -> ApiRepository.aiSummary(languageCode, content) + AiAction.SUMMARY -> ApiRepository.aiSummary(languageCode, mailbox().uuid, messageUid) AiAction.TRANSLATE -> ApiRepository.aiTranslate(languageCode, mailbox().uuid, messageUid) } @@ -563,11 +562,6 @@ class ThreadViewModel @Inject constructor( if (summaryContent != null) saveSummary(messageUid, summaryContent) } - private suspend fun getMessageContent(messageUid: String): String { - val message = messageController.getMessage(messageUid) - return message?.body?.value ?: message?.splitBody?.content ?: "" - } - private fun mapApiResultToState( result: ApiResponse?, isRetryAttempt: Boolean, From 972f513e4c3455dc54d580a549c19262dd358762 Mon Sep 17 00:00:00 2001 From: Elouan BOITEUX Date: Wed, 3 Jun 2026 16:26:33 +0200 Subject: [PATCH 7/7] refactor: Clean code --- .../mail/ui/main/thread/ThreadAdapter.kt | 2 +- .../mail/ui/main/thread/ThreadState.kt | 2 +- .../mail/ui/main/thread/ThreadViewModel.kt | 25 +++++++++---------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt index 4ab300200d..7b6d2e2ec6 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadAdapter.kt @@ -619,7 +619,7 @@ class ThreadAdapter( isButtonEnabled = true hideButtonProgress(R.string.aiButtonRetry) - if (state.isRetryAttempt && !state.wasLoaderShown) { + if (state.asAlreadyRetried && !state.wasLoaderShown) { threadAdapterCallbacks?.showSnackbarRetry?.invoke(errorMessageRes) } } else { diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadState.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadState.kt index ffb2b2a200..6ad770459e 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadState.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadState.kt @@ -74,7 +74,7 @@ sealed class AiProcessState { data class Success(val content: String = "") : AiProcessState() data class Error( val canRetry: Boolean, - val isRetryAttempt: Boolean = true, + val asAlreadyRetried: Boolean = false, val wasLoaderShown: Boolean = false, val targetSameAsSource: Boolean = false ) : AiProcessState() diff --git a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt index 4c9bdb5717..d79ab873b2 100644 --- a/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt +++ b/app/src/main/java/com/infomaniak/mail/ui/main/thread/ThreadViewModel.kt @@ -481,16 +481,16 @@ class ThreadViewModel @Inject constructor( } fun doAiAction(messageUid: String, aiAction: AiAction) { - val isRetryAttempt = getStateMap(aiAction)[messageUid] is AiProcessState.Error + val isRetrying = getStateMap(aiAction)[messageUid] is AiProcessState.Error cancelRetryTimer(messageUid, aiAction) - val initialState = if (isRetryAttempt) AiProcessState.Retrying(isLoaderVisible = false) else AiProcessState.Loading + val initialState = if (isRetrying) AiProcessState.Retrying(isLoaderVisible = false) else AiProcessState.Loading updateAiProcessState(messageUid, aiAction, initialState) - startRetryTimerIfNeeded(messageUid, aiAction, isRetryAttempt) + startRetryTimerIfNeeded(messageUid, aiAction, isRetrying) - viewModelScope.launch { processAiApiCall(messageUid, aiAction, isRetryAttempt) } + viewModelScope.launch { processAiApiCall(messageUid, aiAction, isRetrying) } } fun dismissAiAction(messageUid: String, aiAction: AiAction) { @@ -510,7 +510,7 @@ class ThreadViewModel @Inject constructor( updateAiProcessState(messageUid, aiAction, AiProcessState.Dismissed, bodyUpdate) } - private suspend fun processAiApiCall(messageUid: String, aiAction: AiAction, isRetry: Boolean) { + private suspend fun processAiApiCall(messageUid: String, aiAction: AiAction, isRetrying: Boolean) { val languageCode = appContext.getCurrentLanguageCode() val result = when (aiAction) { @@ -525,7 +525,7 @@ class ThreadViewModel @Inject constructor( val wasLoaderShown = currentState is AiProcessState.Retrying && currentState.isLoaderVisible - val finalState = mapApiResultToState(result, isRetry, wasLoaderShown) + val finalState = mapApiResultToState(result, isRetrying, wasLoaderShown) var bodyUpdate = AiBodyUpdate.NONE if (finalState is AiProcessState.Success) { @@ -564,13 +564,13 @@ class ThreadViewModel @Inject constructor( private fun mapApiResultToState( result: ApiResponse?, - isRetryAttempt: Boolean, + isRetrying: Boolean, wasLoaderShown: Boolean, ): AiProcessState { if (result == null) { return AiProcessState.Error( canRetry = true, - isRetryAttempt = isRetryAttempt, + asAlreadyRetried = isRetrying, wasLoaderShown = wasLoaderShown, targetSameAsSource = false, ) @@ -583,7 +583,7 @@ class ThreadViewModel @Inject constructor( val targetSameAsSource = result.error?.code == ErrorCode.TRANSLATION_TARGET_SAME_AS_SOURCE return AiProcessState.Error( canRetry = canRetry, - isRetryAttempt = isRetryAttempt, + asAlreadyRetried = isRetrying, wasLoaderShown = wasLoaderShown, targetSameAsSource = targetSameAsSource, ) @@ -615,8 +615,8 @@ class ThreadViewModel @Inject constructor( timersMap.remove(messageUid) } - private fun startRetryTimerIfNeeded(messageUid: String, aiAction: AiAction, isRetryAttempt: Boolean) { - if (!isRetryAttempt) return + private fun startRetryTimerIfNeeded(messageUid: String, aiAction: AiAction, isRetrying: Boolean) { + if (!isRetrying) return val timer = UtilsLegacy.createRefreshTimer { val state = getStateMap(aiAction)[messageUid] @@ -625,8 +625,7 @@ class ThreadViewModel @Inject constructor( } } - getTimerMap(aiAction)[messageUid] = timer - timer.start() + getTimerMap(aiAction)[messageUid] = timer.also { it.start() } } private fun cancelAllAiRetryTimers() {