Skip to content
Open
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 @@ -67,6 +67,11 @@ class Attachment : EmbeddedRealmObject, Attachable {
val disposition: AttachmentDisposition?
get() = enumValueOfOrNull<AttachmentDisposition>(_disposition)

fun markAsInline(contentId: String) {
_disposition = AttachmentDisposition.INLINE.name
this.contentId = contentId
}

fun initLocalValues(name: String, size: Long, mimeType: String, uri: String): Attachment {
this.name = name
this.size = size
Expand Down
10 changes: 7 additions & 3 deletions app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -610,13 +610,17 @@ object ApiRepository : ApiRepositoryCore() {
mailbox: Mailbox,
userApiToken: String,
): ApiResponse<Attachment>? {
val attachmentDisposition = attachment.disposition?.name?.lowercase() ?: "attachment"

@OptIn(ManualAuthorizationRequired::class)
val headers = HttpUtils.getHeaders(contentType = null).newBuilder()
val headersBuilder = HttpUtils.getHeaders(contentType = null).newBuilder()
.set("Authorization", "Bearer $userApiToken")
.addUnsafeNonAscii("x-ws-attachment-filename", attachment.name)
.add("x-ws-attachment-mime-type", attachment.mimeType)
.add("x-ws-attachment-disposition", "attachment")
.build()
.add("x-ws-attachment-disposition", attachmentDisposition)
attachment.contentId?.let { headersBuilder.add("x-ws-attachment-content-id", it) }
val headers = headersBuilder.build()

val request = Request.Builder().url(ApiRoutes.createAttachment(mailbox.uuid))
.headers(headers)
.post(attachmentFile.asRequestBody(attachment.mimeType.toMediaType()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,14 @@ fun Attachable.getCacheFile(
}
is SwissTransferFile -> File("")
}

fun Attachment.getInlineCacheFile(
context: Context,
userId: Int = AccountUtils.currentUserId,
mailboxId: Int = AccountUtils.currentMailboxId,
): File {
val cacheFolder = LocalStorageUtils.getAttachmentsCacheDir(context, extractPathFromResource(), userId, mailboxId)
val extension = name.substringAfterLast('.', "")
val uniqueName = if (extension.isNotEmpty()) "$localUuid.$extension" else localUuid
return File(cacheFolder, uniqueName)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package com.infomaniak.mail.data.models.extensions
import androidx.core.net.toFile
import androidx.core.net.toUri
import com.infomaniak.mail.data.models.Attachment
import com.infomaniak.mail.data.models.AttachmentDisposition
import com.infomaniak.mail.data.models.AttachmentUploadStatus
import com.infomaniak.mail.data.models.InternalModelProperties
import com.infomaniak.mail.data.models.draft.Draft
Expand All @@ -42,6 +43,10 @@ fun Attachment.setUploadStatus(attachmentUploadStatus: AttachmentUploadStatus, d
fun Attachment.backupLocalData(oldAttachment: Attachment, draft: Draft) {
localUuid = oldAttachment.localUuid
uploadLocalUri = oldAttachment.uploadLocalUri
contentId = oldAttachment.contentId
if (oldAttachment.disposition == AttachmentDisposition.INLINE) {
oldAttachment.contentId?.let { markAsInline(it) }
}
setUploadStatus(AttachmentUploadStatus.UPLOADED, draft, "backupLocalData -> setUploadStatus")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import com.infomaniak.mail.utils.Utils.runCatchingRealm

class AttachmentAdapter(
private val shouldDisplayCloseButton: Boolean = false,
private val onDelete: ((position: Int) -> Unit)? = null,
private val onDelete: ((attachable: Attachable) -> Unit)? = null,
private val onAttachmentClicked: ((Attachable) -> Unit)? = null,
private val onAttachmentOptionsClicked: ((Attachable) -> Unit)? = null,
) : Adapter<AttachmentViewHolder>() {
Expand Down Expand Up @@ -67,7 +67,7 @@ class AttachmentAdapter(
attachments.removeAt(index)
notifyItemRemoved(index)

onDelete?.invoke(index)
onDelete?.invoke(attachment)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
import android.webkit.WebViewClient
import com.infomaniak.mail.data.api.ApiRepository
import com.infomaniak.mail.data.models.Attachment
import com.infomaniak.mail.data.models.AttachmentDisposition
import com.infomaniak.mail.data.models.extensions.getCacheFile
import com.infomaniak.mail.data.models.extensions.getInlineCacheFile
import com.infomaniak.mail.data.models.extensions.hasUsableCache
import com.infomaniak.mail.utils.LocalStorageUtils
import com.infomaniak.mail.utils.Utils
Expand All @@ -43,13 +45,17 @@

private val emptyResource by lazy { WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream(ByteArray(0))) }

override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? = runCatchingRealm {

Check failure on line 48 in app/src/main/java/com/infomaniak/mail/ui/main/thread/webViewClient/MessageWebViewClient.kt

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Infomaniak_android-mail&issues=AZ_bKPEq3OeTL2UzsCsC&open=AZ_bKPEq3OeTL2UzsCsC&pullRequest=3014
val url = request.url

if (url?.scheme.equals(CID_SCHEME, ignoreCase = true)) {
val cid = url.schemeSpecificPart
return cidDictionary[cid]?.let { attachment ->
val cacheFile = attachment.getCacheFile(context)
val cacheFile = if (attachment.disposition == AttachmentDisposition.INLINE) {
attachment.getInlineCacheFile(context).takeIf { it.exists() } ?: attachment.getCacheFile(context)
} else {
attachment.getCacheFile(context)
}

val data = if (attachment.hasUsableCache(context, cacheFile)) {
cacheFile.inputStream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.infomaniak.mail.MatomoMail.trackEditorActionEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.databinding.FragmentNewMessageBinding
import com.infomaniak.mail.ui.newMessage.encryption.EncryptionMessageManager
import com.infomaniak.mail.utils.SimpleIconPopupMenu
import com.infomaniak.mail.utils.extensions.getAttributeColor
import dagger.hilt.android.scopes.FragmentScoped
import kotlinx.coroutines.launch
Expand All @@ -44,6 +45,7 @@ class NewMessageEditorManager @Inject constructor(private val insertLinkDialog:
private inline val encryptionManager: EncryptionMessageManager get() = _encryptionManager!!

private var _openFilePicker: (() -> Unit)? = null
private var _openPhotoPicker: (() -> Unit)? = null

fun initValues(
newMessageViewModel: NewMessageViewModel,
Expand All @@ -52,6 +54,7 @@ class NewMessageEditorManager @Inject constructor(private val insertLinkDialog:
aiManager: NewMessageAiManager,
encryptionManager: EncryptionMessageManager,
openFilePicker: () -> Unit,
openPhotoPicker: () -> Unit,
) {
super.initValues(
newMessageViewModel = newMessageViewModel,
Expand All @@ -63,6 +66,14 @@ class NewMessageEditorManager @Inject constructor(private val insertLinkDialog:
_aiManager = aiManager
_encryptionManager = encryptionManager
_openFilePicker = openFilePicker
_openPhotoPicker = openPhotoPicker
}

private fun onMenuItemClicked(itemId: Int) {
when (itemId) {
R.id.attachmentPhotoLibrary -> _openPhotoPicker?.invoke()
R.id.attachmentFile -> _openFilePicker?.invoke()
}
}

private fun extractUrl(text: String): String {
Expand All @@ -74,7 +85,7 @@ class NewMessageEditorManager @Inject constructor(private val insertLinkDialog:
fun observeEditorFormatActions() = with(binding) {
newMessageViewModel.editorAction.observe(viewLifecycleOwner) { (editorAction, _) ->
when (editorAction) {
EditorAction.ATTACHMENT -> _openFilePicker?.invoke()
EditorAction.ATTACHMENT -> showAttachmentTypeSelector()
EditorAction.LINK -> handleLink()
EditorAction.AI -> aiManager.openAiPrompt()
EditorAction.BOLD -> editorWebView.toggleBold()
Expand All @@ -87,6 +98,10 @@ class NewMessageEditorManager @Inject constructor(private val insertLinkDialog:
}
}

private fun showAttachmentTypeSelector() {
SimpleIconPopupMenu(context, R.menu.attachment_menu, binding.editorAttachment, ::onMenuItemClicked).show()
}

private fun handleLink() = with(binding) {
if (buttonLink.isActivated) {
editorWebView.unlink()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ 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.models.Attachable
import com.infomaniak.mail.data.models.Attachment
import com.infomaniak.mail.data.models.AttachmentDisposition
import com.infomaniak.mail.data.models.FeatureFlag
Expand All @@ -77,6 +78,9 @@ import com.infomaniak.mail.data.models.correspondent.MergedContact
import com.infomaniak.mail.data.models.draft.Draft
import com.infomaniak.mail.data.models.draft.Draft.DraftMode
import com.infomaniak.mail.data.models.draft.DraftAction
import com.infomaniak.mail.data.models.extensions.getCacheFile
import com.infomaniak.mail.data.models.extensions.getInlineCacheFile
import com.infomaniak.mail.data.models.extensions.getUploadLocalFile
import com.infomaniak.mail.data.models.extensions.kSuite
import com.infomaniak.mail.data.models.mailbox.Mailbox
import com.infomaniak.mail.data.models.signature.Signature
Expand Down Expand Up @@ -106,12 +110,14 @@ import com.infomaniak.mail.utils.HtmlFormatter.Companion.getEditorMentionClickHa
import com.infomaniak.mail.utils.HtmlFormatter.Companion.getEditorMentionsDetectorScript
import com.infomaniak.mail.utils.HtmlFormatter.Companion.getFixStyleScript
import com.infomaniak.mail.utils.HtmlFormatter.Companion.getIncludeQuotesScript
import com.infomaniak.mail.utils.HtmlFormatter.Companion.getInsertInlineImageScript
import com.infomaniak.mail.utils.HtmlFormatter.Companion.getInsertMentionScript
import com.infomaniak.mail.utils.HtmlFormatter.Companion.getMentionDeletionObserverScript
import com.infomaniak.mail.utils.HtmlFormatter.Companion.getMentionsStyle
import com.infomaniak.mail.utils.HtmlFormatter.Companion.getRemoveElementsByIdScript
import com.infomaniak.mail.utils.HtmlFormatter.Companion.getReplaceSignatureScript
import com.infomaniak.mail.utils.HtmlFormatter.Companion.getSetAiContentScript
import com.infomaniak.mail.utils.LocalStorageUtils
import com.infomaniak.mail.utils.MessageBodyUtils.EDITOR_LOCAL_SIGNATURE_ID
import com.infomaniak.mail.utils.MessageBodyUtils.INFOMANIAK_FORWARD_QUOTE_HTML_CLASS_NAME
import com.infomaniak.mail.utils.MessageBodyUtils.INFOMANIAK_REPLY_QUOTE_HTML_CLASS_NAME
Expand Down Expand Up @@ -141,10 +147,12 @@ import dagger.hilt.android.AndroidEntryPoint
import io.sentry.Sentry
import io.sentry.SentryLevel
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import splitties.experimental.ExperimentalSplittiesApi
import java.util.Date
import javax.inject.Inject
Expand All @@ -169,6 +177,7 @@ class NewMessageFragment : Fragment() {
private val fixStyle by lazy { requireContext().getFixStyleScript() }
private val setAiContentScript by lazy { requireContext().getSetAiContentScript() }
private val getEditorBodyScript by lazy { requireContext().getEditorBodyScript() }
private val insertInlineImageScript by lazy { requireContext().getInsertInlineImageScript() }
private val insertMentionScript by lazy { requireContext().getInsertMentionScript() }
private val mentionClickHandlerScript by lazy { requireContext().getEditorMentionClickHandlerScript() }
private val removeElementsByIdScript by lazy { requireContext().getRemoveElementsByIdScript() }
Expand All @@ -187,6 +196,9 @@ class NewMessageFragment : Fragment() {
private val filePicker = FilePicker(fragment = this).apply {
initCallback { uris -> newMessageViewModel.importAttachmentsLiveData.value = uris }
}
private val photoPicker = FilePicker(fragment = this).apply {
initCallback { uris -> newMessageViewModel.importInlineAttachmentsLiveData.value = uris }
}

private var addressListPopupWindow: ListPopupWindow? = null

Expand Down Expand Up @@ -385,6 +397,7 @@ class NewMessageFragment : Fragment() {
aiManager = aiManager,
encryptionManager = encryptionMessageManager,
openFilePicker = filePicker::open,
openPhotoPicker = { photoPicker.open("image/*") },
)

encryptionMessageManager.init(
Expand Down Expand Up @@ -525,6 +538,7 @@ class NewMessageFragment : Fragment() {
addScript(fixStyle)
addScript(editorJsBridgeScript)
addScript(deletedInlineImagesObserverScript)
addScript(insertInlineImageScript)
addScript(mentionClickHandlerScript)
addScript(removeElementsByIdScript)

Expand Down Expand Up @@ -799,6 +813,7 @@ class NewMessageFragment : Fragment() {
if (isFirstTime) {
isFirstTime = false
observeImportAttachments()
observeImportInlineAttachments()
} else if (attachments.count() > attachmentAdapter.itemCount) {
// If we are adding Attachments, directly upload them to save time when sending/saving the Draft.
newMessageViewModel.uploadAttachmentsToServer(attachments)
Expand All @@ -823,11 +838,48 @@ class NewMessageFragment : Fragment() {
importAttachmentsLiveData.observe(viewLifecycleOwner) { uris ->
val currentAttachments = attachmentsLiveData.valueOrEmpty()
importNewAttachments(currentAttachments, uris) { newAttachments ->
attachmentsLiveData.postValue(currentAttachments + newAttachments)
attachmentsLiveData.postValue(attachmentsLiveData.valueOrEmpty() + newAttachments)
}
}
}

private fun observeImportInlineAttachments() = with(newMessageViewModel) {
importInlineAttachmentsLiveData.observe(viewLifecycleOwner) { uris ->
val currentAttachments = attachmentsLiveData.valueOrEmpty()
importNewAttachments(currentAttachments, uris) { newAttachments ->
val inlineAttachments = prepareInlineAttachments(newAttachments)
val updatedAttachments = attachmentsLiveData.valueOrEmpty() + inlineAttachments
attachmentsLiveData.postValue(updatedAttachments)
Comment thread
Elouan1411 marked this conversation as resolved.

viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
inlineAttachments.forEach { attachment ->
attachment.getUploadLocalFile()?.inputStream()?.use { inputStream ->
val cacheFile = attachment.getInlineCacheFile(requireContext())
LocalStorageUtils.saveAttachmentToCacheDir(inputStream, cacheFile)
}
}
}
refreshEditorWebViewClient(updatedAttachments)
inlineAttachments.mapNotNull(Attachment::contentId).forEach { contentId ->
binding.editorWebView.executeJsMethodWhenEditorIsSetup(
JsExecutableMethod("insertInlineImage", contentId),
)
}
}
}
}
}

private fun refreshEditorWebViewClient(attachments: List<Attachment>) {
val alwaysShowExternalContent = localSettings.externalContent == LocalSettings.ExternalContent.ALWAYS
binding.editorWebView.initEditorWebviewClient(
attachments = attachments,
shouldLoadDistantResources = alwaysShowExternalContent || newMessageViewModel.shouldLoadDistantResources(),
onPageFinished = {},
)
}

private fun observeImportAttachmentsResult() = with(newMessageViewModel) {
importAttachmentsResult.observe(viewLifecycleOwner) { result ->
if (result == ImportationResult.ATTACHMENTS_TOO_BIG) showSnackbar(R.string.attachmentFileLimitReached)
Expand Down Expand Up @@ -925,9 +977,9 @@ class NewMessageFragment : Fragment() {
super.onStop()
}

private fun onDeleteAttachment(position: Int) {
private fun onDeleteAttachment(attachable: Attachable) {
trackAttachmentActionsEvent(MatomoName.Delete)
newMessageViewModel.deleteAttachment(position)
if (attachable is Attachment) newMessageViewModel.deleteAttachment(attachable)
}

private fun setupSendButtons(mailbox: Mailbox) = with(binding) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ import kotlinx.coroutines.withContext
import org.jsoup.nodes.Document
import splitties.experimental.ExperimentalSplittiesApi
import java.util.Date
import java.util.UUID
import javax.inject.Inject

@OptIn(ExperimentalCoroutinesApi::class)
Expand Down Expand Up @@ -224,6 +225,7 @@ class NewMessageViewModel @Inject constructor(

//region Attachments
val importAttachmentsLiveData = SingleLiveEvent<List<Uri>>()
val importInlineAttachmentsLiveData = SingleLiveEvent<List<Uri>>()
val importAttachmentsResult = SingleLiveEvent<ImportationResult>()
//endRegion

Expand Down Expand Up @@ -800,6 +802,14 @@ class NewMessageViewModel @Inject constructor(
return newAttachments
}

fun prepareInlineAttachments(attachments: List<Attachment>): List<Attachment> {
attachments.forEach { attachment ->
val contentId = attachment.contentId?.takeIf(String::isNotBlank) ?: "${UUID.randomUUID()}@infomaniak.com"
attachment.markAsInline(contentId)
}
return attachments
}

private suspend fun importAttachment(uri: Uri, availableSpace: Long): Pair<Attachment?, Boolean> {

val (fileName, fileSize) = getFileNameAndSize(uri) ?: return null to false
Expand Down Expand Up @@ -853,21 +863,23 @@ class NewMessageViewModel @Inject constructor(
if (recipient.isDisplayedAsExternal) trackExternalEvent(MatomoName.DeleteRecipient)
}

fun deleteAttachment(position: Int) = viewModelScope.launch(ioCoroutineContext) {
fun deleteAttachment(attachmentToDelete: Attachment) = viewModelScope.launch(ioCoroutineContext) {
runCatching {
val attachments = attachmentsLiveData.valueOrEmpty().toMutableList()
val attachment = attachments[position]
attachment.getUploadLocalFile()?.delete()
LocalStorageUtils.deleteAttachmentUploadDir(appContext, draftLocalUuid!!, attachment.localUuid)
val attachment = attachments.findSpecificAttachment(attachmentToDelete)
attachment?.let {
it.getUploadLocalFile()?.delete()
LocalStorageUtils.deleteAttachmentUploadDir(appContext, draftLocalUuid!!, it.localUuid)

mailboxContentRealm().write {
DraftController.updateDraftBlocking(draftLocalUuid!!, realm = this) {
it.attachments.findSpecificAttachment(attachment)?.let(::delete)
mailboxContentRealm().write {
DraftController.updateDraftBlocking(draftLocalUuid!!, realm = this) { draft ->
draft.attachments.findSpecificAttachment(it)?.let(::delete)
}
}
}

attachments.removeAt(position)
attachmentsLiveData.postValue(attachments)
attachments.remove(it)
attachmentsLiveData.postValue(attachments)
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/infomaniak/mail/utils/HtmlFormatter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,10 @@ class HtmlFormatter(private val html: String) {
return loadScript(R.raw.insert_mention)
}

fun Context.getInsertInlineImageScript(): String {
return loadScript(R.raw.insert_inline_image)
}

fun Context.getEditorMentionClickHandlerScript(): String {
return loadScript(R.raw.editor_mention_click_handler)
}
Expand Down
Loading
Loading