Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import com.infomaniak.mail.ui.main.emojiPicker.PickedEmojiPayload
import com.infomaniak.mail.ui.main.emojiPicker.PickerEmojiObserver
import com.infomaniak.mail.ui.main.folder.ThreadListViewModel.ContentDisplayMode
import com.infomaniak.mail.ui.main.thread.ThreadFragment
import com.infomaniak.mail.ui.main.thread.actions.EmojiReactionsViewModel
import com.infomaniak.mail.ui.newMessage.NewMessageActivityArgs
import com.infomaniak.mail.utils.AccountUtils
import com.infomaniak.mail.utils.FolderRoleUtils
Expand Down Expand Up @@ -120,6 +121,7 @@ class ThreadListFragment : TwoPaneFragment(), PickerEmojiObserver {

private val navigationArgs: ThreadListFragmentArgs by navArgs()
private val threadListViewModel: ThreadListViewModel by viewModels()
private val emojiReactionsViewModel: EmojiReactionsViewModel by viewModels()

override val substituteClassName: String = javaClass.name

Expand Down Expand Up @@ -705,7 +707,7 @@ class ThreadListFragment : TwoPaneFragment(), PickerEmojiObserver {
trackEmojiReactionsEvent(MatomoName.AddReactionFromEmojiPicker)
viewLifecycleOwner.lifecycleScope.launch {
threadListViewModel.getEmojiReactionsFor(messageUid)?.let { reactions ->
actionsViewModel.trySendEmojiReply(
emojiReactionsViewModel.trySendEmojiReply(
emoji = emoji,
messageUid = messageUid,
reactions = reactions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import com.infomaniak.mail.ui.main.thread.ThreadViewModel.ThreadHeaderVisibility
import com.infomaniak.mail.ui.main.thread.actions.ActionsViewModel
import com.infomaniak.mail.ui.main.thread.actions.AttachmentActionsBottomSheetDialogArgs
import com.infomaniak.mail.ui.main.thread.actions.ConfirmationToBlockUserDialog
import com.infomaniak.mail.ui.main.thread.actions.EmojiReactionsViewModel
import com.infomaniak.mail.ui.main.thread.actions.JunkMessagesViewModel
import com.infomaniak.mail.ui.main.thread.actions.MessageActionsBottomSheetDialogArgs
import com.infomaniak.mail.ui.main.thread.actions.ReplyBottomSheetDialogArgs
Expand Down Expand Up @@ -198,6 +199,7 @@ class ThreadFragment : Fragment(), PickerEmojiObserver {
private val twoPaneViewModel: TwoPaneViewModel by activityViewModels()
private val threadViewModel: ThreadViewModel by viewModels()
private val actionsViewModel: ActionsViewModel by activityViewModels()
private val emojiReactionsViewModel: EmojiReactionsViewModel by viewModels()

private val twoPaneFragment inline get() = parentFragment as TwoPaneFragment
private val threadAdapter inline get() = binding.messagesList.adapter as ThreadAdapter
Expand Down Expand Up @@ -426,7 +428,7 @@ class ThreadFragment : Fragment(), PickerEmojiObserver {
trackEmojiReactionsEvent(MatomoName.AddReactionFromChip)
}

actionsViewModel.trySendEmojiReply(
emojiReactionsViewModel.trySendEmojiReply(
emoji = emoji,
messageUid = messageUid,
reactions = reactions,
Expand Down Expand Up @@ -677,7 +679,7 @@ class ThreadFragment : Fragment(), PickerEmojiObserver {
getBackNavigationResult<PickedEmojiPayload>(EmojiPickerObserverTarget.Thread.name) { (emoji, messageUid) ->
trackEmojiReactionsEvent(MatomoName.AddReactionFromEmojiPicker)
val reactions = threadViewModel.getLocalEmojiReactionsFor(messageUid) ?: return@getBackNavigationResult
actionsViewModel.trySendEmojiReply(
emojiReactionsViewModel.trySendEmojiReply(
emoji = emoji,
messageUid = messageUid,
reactions = reactions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,16 @@
package com.infomaniak.mail.ui.main.thread.actions

import android.app.Application
import androidx.annotation.StringRes
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.infomaniak.core.legacy.utils.SingleLiveEvent
import com.infomaniak.core.network.models.ApiResponse
import com.infomaniak.core.network.utils.ApiErrorCode.Companion.translateError
import com.infomaniak.emojicomponents.data.Reaction
import com.infomaniak.mail.R
import com.infomaniak.mail.data.LocalSettings
import com.infomaniak.mail.data.api.ApiRepository
import com.infomaniak.mail.data.cache.RealmDatabase
import com.infomaniak.mail.data.cache.mailboxContent.DraftController
import com.infomaniak.mail.data.cache.mailboxContent.FolderController
import com.infomaniak.mail.data.cache.mailboxContent.ImpactedFolders
import com.infomaniak.mail.data.cache.mailboxContent.MessageController
Expand All @@ -40,7 +37,6 @@ import com.infomaniak.mail.data.cache.mailboxInfo.MailboxController
import com.infomaniak.mail.data.models.Folder
import com.infomaniak.mail.data.models.Folder.FolderRole
import com.infomaniak.mail.data.models.MoveResult
import com.infomaniak.mail.data.models.draft.Draft
import com.infomaniak.mail.data.models.isSnoozed
import com.infomaniak.mail.data.models.mailbox.Mailbox
import com.infomaniak.mail.data.models.mailbox.SendersRestrictions
Expand All @@ -50,15 +46,10 @@ import com.infomaniak.mail.data.models.thread.Thread
import com.infomaniak.mail.di.IoDispatcher
import com.infomaniak.mail.ui.main.SnackbarManager
import com.infomaniak.mail.ui.main.SnackbarManager.UndoData
import com.infomaniak.mail.utils.AccountUtils
import com.infomaniak.mail.utils.DraftInitManager
import com.infomaniak.mail.utils.EmojiReactionUtils.hasAvailableReactionSlot
import com.infomaniak.mail.utils.ErrorCode
import com.infomaniak.mail.utils.FeatureAvailability
import com.infomaniak.mail.utils.FolderRoleUtils
import com.infomaniak.mail.utils.SharedUtils
import com.infomaniak.mail.utils.SharedUtils.Companion.unsnoozeThreadsWithoutRefresh
import com.infomaniak.mail.utils.Utils
import com.infomaniak.mail.utils.Utils.isPermanentDeleteFolder
import com.infomaniak.mail.utils.coroutineContext
import com.infomaniak.mail.utils.date.DateFormatUtils.dayOfWeekDateWithoutYear
Expand All @@ -69,7 +60,6 @@ import com.infomaniak.mail.utils.extensions.atLeastOneSucceeded
import com.infomaniak.mail.utils.extensions.getFirstTranslatedError
import com.infomaniak.mail.utils.extensions.getFoldersIds
import com.infomaniak.mail.utils.extensions.getUids
import com.infomaniak.mail.workers.DraftsActionsWorker
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
Expand All @@ -83,9 +73,6 @@ import com.infomaniak.core.legacy.R as RCore
@HiltViewModel
class ActionsViewModel @Inject constructor(
application: Application,
private val draftController: DraftController,
private val draftInitManager: DraftInitManager,
private val draftsActionsWorkerScheduler: DraftsActionsWorker.Scheduler,
private val folderController: FolderController,
private val folderRoleUtils: FolderRoleUtils,
private val localSettings: LocalSettings,
Expand Down Expand Up @@ -764,87 +751,6 @@ class ActionsViewModel @Inject constructor(
}
//endregion

//region Emoji reaction
/**
* Wrapper method to send an emoji reaction to the api. This method will check if the emoji reaction is allowed before
* initiating an api call. This is the entry point to add an emoji reaction anywhere in the app.
*
* If sending is allowed, the caller place can fake the emoji reaction locally thanks to [onAllowed].
* If sending is not allowed, it will display the error directly to the user and avoid doing the api call.
*/
fun trySendEmojiReply(
emoji: String,
messageUid: String,
reactions: Map<String, Reaction>,
hasNetwork: Boolean,
mailbox: Mailbox,
onAllowed: () -> Unit = {},
) {
viewModelScope.launch {
when (val status = reactions.getEmojiSendStatus(emoji, hasNetwork)) {
EmojiSendStatus.Allowed -> {
onAllowed()
sendEmojiReply(emoji, messageUid, mailbox)
}
is EmojiSendStatus.NotAllowed -> snackbarManager.postValue(appContext.getString(status.errorMessageRes))
}
}
}

private fun Map<String, Reaction>.getEmojiSendStatus(emoji: String, hasNetwork: Boolean): EmojiSendStatus = when {
this[emoji]?.hasReacted == true -> EmojiSendStatus.NotAllowed.AlreadyUsed
hasAvailableReactionSlot().not() -> EmojiSendStatus.NotAllowed.MaxReactionReached
hasNetwork.not() -> EmojiSendStatus.NotAllowed.NoInternet
else -> EmojiSendStatus.Allowed
}

/**
* The actual logic of sending an emoji reaction to the api. This method initializes a [Draft] instance, stores it into the
* database and schedules the [DraftsActionsWorker] so the draft is uploaded on the api.
*/
private suspend fun sendEmojiReply(emoji: String, messageUid: String, mailbox: Mailbox) {
val targetMessage = messageController.getMessage(messageUid) ?: return
val (fullMessage, hasFailedFetching) = draftController.fetchHeavyDataIfNeeded(targetMessage)
if (hasFailedFetching) return

val draftMode = Draft.DraftMode.REPLY_ALL
val draft = Draft().apply {
with(draftInitManager) {
setPreviousMessage(draftMode, fullMessage)
}

val quote = draftInitManager.createQuote(draftMode, fullMessage, attachments)
body = EMOJI_REACTION_PLACEHOLDER + quote

with(draftInitManager) {
// We don't want to send the HTML code of the signature for an emoji reaction but we still need to send the
// identityId stored in a Signature
val signature = chooseSignature(mailbox.email, mailbox.signatures, draftMode, fullMessage)
setSignatureIdentity(signature)
}

mimeType = Utils.TEXT_HTML

action = Draft.DraftAction.SEND_REACTION
emojiReaction = emoji
}

draftController.upsertDraft(draft)

draftsActionsWorkerScheduler.scheduleWork(draft.localUuid, AccountUtils.currentMailboxId, AccountUtils.currentUserId)
}

private sealed interface EmojiSendStatus {
data object Allowed : EmojiSendStatus

sealed class NotAllowed(@StringRes val errorMessageRes: Int) : EmojiSendStatus {
data object AlreadyUsed : NotAllowed(ErrorCode.EmojiReactions.alreadyUsed.translateRes)
data object MaxReactionReached : NotAllowed(ErrorCode.EmojiReactions.maxReactionReached.translateRes)
data object NoInternet : NotAllowed(RCore.string.noConnection)
}
}
//endregion

//region Undo action
fun undoAction(undoData: UndoData, mailbox: Mailbox) = viewModelScope.launch(ioCoroutineContext) {

Expand Down Expand Up @@ -904,8 +810,4 @@ class ActionsViewModel @Inject constructor(
if (uidsToMove.isNotEmpty()) threadController.updateIsLocallyMovedOutStatus(uidsToMove, hasBeenMovedOut = true)
return uidsToMove
}

companion object {
private const val EMOJI_REACTION_PLACEHOLDER = "<div>__REACTION_PLACEMENT__<br></div>"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.mail.ui.main.thread.actions

import android.app.Application
import androidx.annotation.StringRes
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.infomaniak.emojicomponents.data.Reaction
import com.infomaniak.mail.data.cache.mailboxContent.DraftController
import com.infomaniak.mail.data.cache.mailboxContent.MessageController
import com.infomaniak.mail.data.models.draft.Draft
import com.infomaniak.mail.data.models.mailbox.Mailbox
import com.infomaniak.mail.ui.main.SnackbarManager
import com.infomaniak.mail.utils.AccountUtils
import com.infomaniak.mail.utils.DraftInitManager
import com.infomaniak.mail.utils.EmojiReactionUtils.hasAvailableReactionSlot
import com.infomaniak.mail.utils.ErrorCode
import com.infomaniak.mail.utils.Utils
import com.infomaniak.mail.utils.extensions.appContext
import com.infomaniak.mail.workers.DraftsActionsWorker
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
import com.infomaniak.core.legacy.R as RCore

@HiltViewModel
class EmojiReactionsViewModel @Inject constructor(
application: Application,
private val draftController: DraftController,
private val draftInitManager: DraftInitManager,
private val draftsActionsWorkerScheduler: DraftsActionsWorker.Scheduler,
private val messageController: MessageController,
private val snackbarManager: SnackbarManager,
) : AndroidViewModel(application) {
/**
* Wrapper method to send an emoji reaction to the api. This method will check if the emoji reaction is allowed before
* initiating an api call. This is the entry point to add an emoji reaction anywhere in the app.
*
* If sending is allowed, the caller place can fake the emoji reaction locally thanks to [onAllowed].
* If sending is not allowed, it will display the error directly to the user and avoid doing the api call.
*/
fun trySendEmojiReply(
emoji: String,
messageUid: String,
reactions: Map<String, Reaction>,
hasNetwork: Boolean,
mailbox: Mailbox,
onAllowed: () -> Unit = {},
) {
viewModelScope.launch {
when (val status = reactions.getEmojiSendStatus(emoji, hasNetwork)) {
EmojiSendStatus.Allowed -> {
onAllowed()
sendEmojiReply(emoji, messageUid, mailbox)
}
is EmojiSendStatus.NotAllowed -> snackbarManager.postValue(appContext.getString(status.errorMessageRes))
}
}
}

private fun Map<String, Reaction>.getEmojiSendStatus(emoji: String, hasNetwork: Boolean): EmojiSendStatus = when {
this[emoji]?.hasReacted == true -> EmojiSendStatus.NotAllowed.AlreadyUsed
hasAvailableReactionSlot().not() -> EmojiSendStatus.NotAllowed.MaxReactionReached
hasNetwork.not() -> EmojiSendStatus.NotAllowed.NoInternet
else -> EmojiSendStatus.Allowed
}

/**
* The actual logic of sending an emoji reaction to the api. This method initializes a [Draft] instance, stores it into the
* database and schedules the [DraftsActionsWorker] so the draft is uploaded on the api.
*/
private suspend fun sendEmojiReply(emoji: String, messageUid: String, mailbox: Mailbox) {
val targetMessage = messageController.getMessage(messageUid) ?: return
val (fullMessage, hasFailedFetching) = draftController.fetchHeavyDataIfNeeded(targetMessage)
if (hasFailedFetching) return

val draftMode = Draft.DraftMode.REPLY_ALL
val draft = Draft().apply {
with(draftInitManager) {
setPreviousMessage(draftMode, fullMessage)
}

val quote = draftInitManager.createQuote(draftMode, fullMessage, attachments)
body = EMOJI_REACTION_PLACEHOLDER + quote

with(draftInitManager) {
// We don't want to send the HTML code of the signature for an emoji reaction but we still need to send the
// identityId stored in a Signature
val signature = chooseSignature(mailbox.email, mailbox.signatures, draftMode, fullMessage)
setSignatureIdentity(signature)
}

mimeType = Utils.TEXT_HTML

action = Draft.DraftAction.SEND_REACTION
emojiReaction = emoji
}

draftController.upsertDraft(draft)

draftsActionsWorkerScheduler.scheduleWork(draft.localUuid, AccountUtils.currentMailboxId, AccountUtils.currentUserId)
}

private sealed interface EmojiSendStatus {
data object Allowed : EmojiSendStatus

sealed class NotAllowed(@StringRes val errorMessageRes: Int) : EmojiSendStatus {
data object AlreadyUsed : NotAllowed(ErrorCode.EmojiReactions.alreadyUsed.translateRes)
data object MaxReactionReached : NotAllowed(ErrorCode.EmojiReactions.maxReactionReached.translateRes)
data object NoInternet : NotAllowed(RCore.string.noConnection)
}
}

companion object {
private const val EMOJI_REACTION_PLACEHOLDER = "<div>__REACTION_PLACEMENT__<br></div>"
}
}
Loading