/* * Copyright (c) 2022 Proton AG * * This file is part of Proton Mail. * * Proton Mail 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. * * Proton Mail 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 Proton Mail. If not, see https://www.gnu.org/licenses/. */ package ch.protonmail.android.activities.messageDetails import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.os.Build import android.text.method.LinkMovementMethod import android.text.util.Linkify import android.view.Gravity import android.view.LayoutInflater import android.view.ScaleGestureDetector import android.view.View import android.view.ViewConfiguration import android.view.ViewGroup import android.webkit.WebSettings import android.webkit.WebView import android.widget.Button import android.widget.LinearLayout import android.widget.ProgressBar import android.widget.TextView import androidx.core.view.isVisible import androidx.core.view.postDelayed import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.RecyclerView import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature import ch.protonmail.android.R import ch.protonmail.android.activities.messageDetails.attachments.MessageDetailsAttachmentListAdapter import ch.protonmail.android.activities.messageDetails.body.MessageBodyScaleListener import ch.protonmail.android.activities.messageDetails.body.MessageBodyTouchListener import ch.protonmail.android.core.Constants import ch.protonmail.android.core.UserManager import ch.protonmail.android.data.local.model.Attachment import ch.protonmail.android.data.local.model.Message import ch.protonmail.android.details.domain.model.SignatureVerification import ch.protonmail.android.details.presentation.mapper.MessageEncryptionUiModelMapper import ch.protonmail.android.details.presentation.mapper.MessageToMessageDetailsListItemMapper import ch.protonmail.android.details.presentation.model.ConversationUiModel import ch.protonmail.android.details.presentation.model.MessageDetailsListItem import ch.protonmail.android.details.presentation.ui.MessageDetailsActivity import ch.protonmail.android.details.presentation.view.MessageDetailsActionsView import ch.protonmail.android.labels.domain.model.Label import ch.protonmail.android.settings.data.AccountSettingsRepository import ch.protonmail.android.ui.model.LabelChipUiModel import ch.protonmail.android.util.ProtonCalendarUtil import ch.protonmail.android.utils.redirectToChrome import ch.protonmail.android.utils.ui.ExpandableRecyclerAdapter import ch.protonmail.android.utils.ui.TYPE_HEADER import ch.protonmail.android.utils.ui.TYPE_ITEM import ch.protonmail.android.utils.webview.SetUpWebViewDarkModeHandlingIfSupported import ch.protonmail.android.views.PmWebViewClient import ch.protonmail.android.views.messageDetails.MessageDetailsAttachmentsView import ch.protonmail.android.views.messageDetails.MessageDetailsHeaderView import ch.protonmail.android.views.messageDetails.ReplyActionsView import kotlinx.android.synthetic.main.layout_message_details.view.* import kotlinx.android.synthetic.main.layout_message_details_body.view.* import kotlinx.coroutines.runBlocking import org.apache.http.protocol.HTTP import timber.log.Timber /** * The delay after which we check if we can switch from a fixed message content height to WRAP_CONTENT. This behaviour * is needed in order to keep the message header attached to the top of the screen while the content is being loaded. */ private const val WRAP_MESSAGE_CONTENT_DELAY_MS = 100L private const val ITEM_NOT_FOUND_INDEX = -1 internal class MessageDetailsAdapter( private val context: Context, private var messages: List, private val messageDetailsRecyclerView: RecyclerView, private val messageToMessageDetailsListItemMapper: MessageToMessageDetailsListItemMapper, private val userManager: UserManager, private val accountSettingsRepository: AccountSettingsRepository, private val messageEncryptionUiModelMapper: MessageEncryptionUiModelMapper, private val setUpWebViewDarkModeHandlingIfSupported: SetUpWebViewDarkModeHandlingIfSupported, private val protonCalendarUtil: ProtonCalendarUtil, private val onLoadEmbeddedImagesClicked: (Message, List) -> Unit, private val onDisplayRemoteContentClicked: (Message) -> Unit, private val onLoadMessageBody: (Message) -> Unit, private val onAttachmentDownloadCallback: (Attachment) -> Unit, private val onOpenInProtonCalendarClicked: (Message) -> Unit, private val onEditDraftClicked: (Message) -> Unit, private val onReplyMessageClicked: (Constants.MessageActionType, Message) -> Unit, private val onMoreMessageActionsClicked: (Message) -> Unit ) : ExpandableRecyclerAdapter(context) { private var exclusiveLabelsPerMessage: HashMap> = hashMapOf() private var nonExclusiveLabelsPerMessage: HashMap> = hashMapOf() private val messageLoadingSpinnerTopMargin by lazy { context.resources.getDimension(R.dimen.padding_m).toInt() } private val messageContentFixedLoadingHeight by lazy { context.resources.getDimension(R.dimen.constrained_message_content_view_size).toInt() } override fun onBindViewHolder(holder: ViewHolder, position: Int) { if (getItemViewType(position) == TYPE_HEADER) { (holder as HeaderViewHolder).bind( visibleItems[position].message ) } else { val isLastNonDraftItemInTheList = position == visibleItems.indexOfLast { !it.message.isDraft() } (holder as ItemViewHolder).bind( position, visibleItems[position] as MessageDetailsListItem.Body, isLastNonDraftItemInTheList ) } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return if (viewType == TYPE_HEADER) { HeaderViewHolder( LayoutInflater.from(context).inflate( R.layout.layout_message_details, parent, false ) ) } else { val itemView = LayoutInflater.from(context).inflate( R.layout.layout_message_details_body, parent, false ) val webView = setupMessageBodyWebView(itemView) itemView.messageWebViewContainer.removeAllViews() itemView.messageWebViewContainer.addView(webView) val messageBodyProgress = createMessageBodyProgressBar() itemView.messageWebViewContainer.addView(messageBodyProgress) val detailsMessageActions = createInMessageActionsView() itemView.messageWebViewContainer.addView(detailsMessageActions) val replyActionsView = createReplyActionsView() itemView.messageWebViewContainer.addView(replyActionsView) setInitialMessageBodyHeight(itemView.messageWebViewContainer) ItemViewHolder(itemView) } } private fun setInitialMessageBodyHeight(messageWebViewContainer: LinearLayout) { // For single message conversations, we don't need to artificially expand the height while loading, as the // top of the only message will always be attached to the top of the screen. For multi-message conversations, // we need to expand it to make sure it occupies enough space on the screen to stay attached to the top // until the content is loaded. if (showingMoreThanOneMessage()) { setMessageContentExpandedLoadingHeight(messageWebViewContainer) } else { setMessageContentFixedLoadingHeight(messageWebViewContainer) } } private fun createMessageBodyProgressBar(): ProgressBar { return ProgressBar(context).apply { id = R.id.item_message_body_progress_view_id layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT ).apply { gravity = Gravity.CENTER_HORIZONTAL setMargins(0, messageLoadingSpinnerTopMargin, 0, 0) } } } private fun showingMoreThanOneMessage() = messages.size > 1 private fun createInMessageActionsView(): MessageDetailsActionsView { val detailsMessageActions = MessageDetailsActionsView(context) detailsMessageActions.id = R.id.item_message_body_actions_layout_id detailsMessageActions.isVisible = false return detailsMessageActions } private fun createReplyActionsView(): ReplyActionsView { val replyActionsView = ReplyActionsView(context) replyActionsView.id = R.id.item_message_body_reply_actions_layout_id replyActionsView.isVisible = false return replyActionsView } private fun setupMessageBodyWebView(itemView: View): WebView? { val context = context as MessageDetailsActivity // Looks like some devices are not able to create a WebView in some conditions. // Show Toast and redirect to the proper page. val webView = try { WebView(context) } catch (ignored: Throwable) { (context as FragmentActivity).redirectToChrome() return null } webView.id = R.id.item_message_body_web_view_id val webViewClient = MessageDetailsPmWebViewClient( userManager, accountSettingsRepository, context, itemView, shouldShowRemoteImages() ) configureWebView(webView, webViewClient) setUpScrollListener(webView, itemView.messageWebViewContainer) webView.invalidate() context.registerForContextMenu(webView) return webView } override fun onViewRecycled(holder: ViewHolder) { super.onViewRecycled(holder) holder.itemView.messageWebViewContainer?.let { resetWebViewContent(it) } holder.itemView.lastConversationMessageCollapsedDivider?.let { it.isVisible = false } holder.itemView.headerView?.forbidExpandingHeaderView() holder.itemView.headerView?.hideRecipientsCollapsedView() } fun showMessageDetails( parsedBody: String?, messageId: String, showLoadEmbeddedImagesButton: Boolean, showDecryptionError: Boolean, attachments: List, embeddedImageIds: List, hasValidSignature: Boolean, hasInvalidSignature: Boolean ) { val item: MessageDetailsListItem? = visibleItems.firstOrNull { it.itemType == TYPE_ITEM && it.message.messageId == messageId } if (item == null) { Timber.d("Trying to show $messageId details but message is not in visibleItems list") return } Timber.d("Show message details: $messageId") val validParsedBody = parsedBody ?: return val message = item.message.apply { setAttachmentList(attachments) // Mark the message as read optimistically to reflect the change on the UI right away. // Note that this message is being referenced to from both the header and the item. this.Unread = false this.hasInvalidSignature = hasInvalidSignature this.hasValidSignature = hasValidSignature } val newItem = messageToMessageDetailsListItemMapper.toMessageDetailsListItem( message, validParsedBody, showDecryptionError, showLoadEmbeddedImagesButton, ).copy(embeddedImageIds = embeddedImageIds) visibleItems.indexOf(item).let { changedItemIndex -> visibleItems[changedItemIndex] = newItem // Update both the message and its header to ensure the "read" status is shown val messageHeaderIndex = changedItemIndex - 1 notifyItemRangeChanged(messageHeaderIndex, 2) } } fun setMessageData(conversation: ConversationUiModel) { messages = conversation.messages exclusiveLabelsPerMessage = conversation.exclusiveLabels nonExclusiveLabelsPerMessage = conversation.nonExclusiveLabels val items = ArrayList() messages.forEach { message -> val header = MessageDetailsListItem.Header(message) val body = MessageDetailsListItem.Body( message = message, messageFormattedHtml = message.decryptedHTML, messageFormattedHtmlWithQuotedHistory = message.decryptedHTML, showOpenInProtonCalendar = protonCalendarUtil.hasCalendarAttachment(message), showLoadEmbeddedImagesButton = false, showDecryptionError = false ) items.add(header) items.add(body) } setItems(items) expandLastNonDraftItem() } fun reloadMessage(messageId: String) { val item = visibleItems .filterIsInstance() .find { it.message.messageId == messageId } ?: return val itemIndex = visibleItems.indexOf(item as MessageDetailsListItem) // Set message formatted html to null, in order to trigger loading // and switch to light/dark mode in the web view val newItem = item.copy(messageFormattedHtml = null) visibleItems[itemIndex] = newItem notifyItemChanged(itemIndex) } private fun expandLastNonDraftItem() { val lastNonDraftHeaderIndex = visibleItems.indexOfLast { !it.message.isDraft() && it.itemType == TYPE_HEADER } if (lastNonDraftHeaderIndex == ITEM_NOT_FOUND_INDEX) { return } if (!isExpanded(lastNonDraftHeaderIndex)) { toggleExpandedItems(lastNonDraftHeaderIndex, true) } } @SuppressLint("ClickableViewAccessibility") private fun setUpScrollListener(webView: WebView, directParent: LinearLayout) { val mScaleDetector = ScaleGestureDetector( context, MessageBodyScaleListener( messageDetailsRecyclerView, webView, directParent ) ) val scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop val touchListener = MessageBodyTouchListener(messageDetailsRecyclerView, mScaleDetector, scaledTouchSlop) messageDetailsRecyclerView.setOnTouchListener(touchListener) webView.setOnTouchListener(touchListener) } private fun displayAttachmentInfo( attachments: List?, attachmentsView: MessageDetailsAttachmentsView ) { if (attachments == null) { attachmentsView.visibility = View.GONE return } val attachmentsCount = attachments.size val totalAttachmentSize = attachments.map { it.fileSize }.sum() val attachmentsListAdapter = MessageDetailsAttachmentListAdapter( context, onAttachmentDownloadCallback ) attachmentsListAdapter.setList(attachments) attachmentsView.bind(attachmentsCount, totalAttachmentSize, attachmentsListAdapter) attachmentsView.isVisible = attachmentsCount > 0 } private fun configureWebView(webView: WebView, pmWebViewClient: PmWebViewClient) { webView.isScrollbarFadingEnabled = false webView.isVerticalScrollBarEnabled = false webView.isHorizontalScrollBarEnabled = false val webViewParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) webViewParams.setMargins(0, 0, 0, 0) webView.layoutParams = webViewParams webView.webViewClient = pmWebViewClient webView.tag = "messageWebView" val webSettings = webView.settings webSettings.layoutAlgorithm = WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING webSettings.useWideViewPort = true webSettings.loadWithOverviewMode = true webSettings.allowFileAccess = false webSettings.displayZoomControls = false webSettings.setGeolocationEnabled(false) webSettings.savePassword = false webSettings.javaScriptEnabled = false webSettings.setSupportZoom(true) webSettings.builtInZoomControls = true webSettings.pluginState = WebSettings.PluginState.OFF webSettings.setNeedInitialFocus(false) webSettings.setRenderPriority(WebSettings.RenderPriority.HIGH) webSettings.cacheMode = WebSettings.LOAD_NO_CACHE webSettings.setAppCacheEnabled(false) webSettings.saveFormData = false webView.setOnLongClickListener { val messageBodyWebView = it as WebView val result = messageBodyWebView.hitTestResult if (result.type == WebView.HitTestResult.SRC_ANCHOR_TYPE) { (context as Activity).openContextMenu(messageBodyWebView) true } else { false } } } private fun resetWebViewContent(messageWebViewContainer: LinearLayout) { val webView = messageWebViewContainer.findViewById(R.id.item_message_body_web_view_id) webView?.loadUrl("about:blank") } private fun setMessageContentFixedLoadingHeight(messageWebViewContainer: LinearLayout) { val params = messageWebViewContainer.layoutParams params.height = messageContentFixedLoadingHeight messageWebViewContainer.layoutParams = params } private fun setMessageContentExpandedLoadingHeight(messageWebViewContainer: LinearLayout) { messageWebViewContainer.layoutParams.height = (context.resources.displayMetrics.heightPixels * 0.7).toInt() } private fun wrapMessageContentHeight(messageWebViewContainer: LinearLayout) { val params = messageWebViewContainer.layoutParams params.height = ViewGroup.LayoutParams.WRAP_CONTENT messageWebViewContainer.layoutParams = params } private fun wrapMessageContentHeightWhenContentLoaded(messageWebViewContainer: LinearLayout) { messageWebViewContainer.postDelayed(WRAP_MESSAGE_CONTENT_DELAY_MS) { val webView = messageWebViewContainer.findViewById(R.id.item_message_body_web_view_id) // We want to keep waiting until the content is actually loaded before wrapping the height to // avoid wrapping to quickly (this could result in the message being scrolled behind the screen instead // of staying in the user's view) if (webView.contentHeight != 0) { wrapMessageContentHeight(messageWebViewContainer) } else { wrapMessageContentHeightWhenContentLoaded(messageWebViewContainer) } } } private fun setUpSpamScoreView(spamScore: Int, spamScoreView: TextView) { val spamScoreVisibility: Int if (listOf(100, 101, 102).contains(spamScore)) { val spamScoreText = getSpamScoreText(spamScore) spamScoreView.setText(spamScoreText) Linkify.addLinks(spamScoreView, Linkify.ALL) spamScoreView.movementMethod = LinkMovementMethod.getInstance() spamScoreVisibility = View.VISIBLE } else { spamScoreVisibility = View.GONE } spamScoreView.visibility = spamScoreVisibility } private fun getSpamScoreText(spamScore: Int): Int { return when (spamScore) { 100 -> R.string.spam_score_100 101 -> R.string.spam_score_101 102 -> R.string.spam_score_102 else -> throw IllegalArgumentException("Unknown spam score.") } } private class MessageDetailsPmWebViewClient( userManager: UserManager, accountSettingsRepository: AccountSettingsRepository, activity: Activity, private val itemView: View, private val isAutoShowRemoteImages: Boolean ) : PmWebViewClient(userManager, accountSettingsRepository, activity, isAutoShowRemoteImages) { override fun onPageFinished(view: WebView, url: String) { if (amountOfRemoteResourcesBlocked() > 0) { itemView.displayRemoteContentButton.isVisible = true } this.blockRemoteResources(!isAutoShowRemoteImages) super.onPageFinished(view, url) } } private fun shouldShowRemoteImages(): Boolean { val mailSettings = userManager.getCurrentUserMailSettingsBlocking() val isAutoShowRemoteImages = mailSettings?.showImagesFrom?.includesRemote() ?: false // When android API < 26 we automatically show remote images because the `getWebViewClient` method // that we use to access the webView and load them later on was only introduced with API 26 return isAutoShowRemoteImages || isAndroidApiLevelLowerThan26() } private fun isAndroidApiLevelLowerThan26() = Build.VERSION.SDK_INT < Build.VERSION_CODES.O private fun setUpWebViewDarkModeBlocking(webView: WebView, messageId: String) = runBlocking { setUpWebViewDarkModeHandlingIfSupported(context, userManager.requireCurrentUserId(), webView, messageId) } inner class HeaderViewHolder( view: View ) : ExpandableRecyclerAdapter.HeaderViewHolder(view) { fun bind(message: Message) { val messageDetailsHeaderView = itemView.headerView val signatureVerification = when { message.hasValidSignature -> SignatureVerification.SUCCESSFUL message.hasInvalidSignature -> SignatureVerification.FAILED else -> SignatureVerification.UNKNOWN } val messageEncryptionStatus = messageEncryptionUiModelMapper.messageEncryptionToUiModel( message.messageEncryption, signatureVerification, message.isSent ) messageDetailsHeaderView.bind( message, messageEncryptionStatus, exclusiveLabelsPerMessage[message.messageId] ?: listOf(), nonExclusiveLabelsPerMessage[message.messageId] ?: listOf(), ::onHeaderCollapsed ) messageDetailsHeaderView.setOnClickListener { view -> // For single message we don't want to allow collapsing the message if (!showingMoreThanOneMessage()) return@setOnClickListener val headerView = view as MessageDetailsHeaderView if (isMessageBodyExpanded()) { // Message Body is expended - will collapse headerView.collapseHeader() } toggleExpandedItems(layoutPosition, false) notifyItemChanged(layoutPosition) } if (isMessageBodyExpanded()) { messageDetailsHeaderView.allowExpandingHeaderView() messageDetailsHeaderView.showRecipientsCollapsedView() messageDetailsHeaderView.hideCollapsedMessageViews() } else { messageDetailsHeaderView.hideRecipientsCollapsedView() messageDetailsHeaderView.showCollapsedMessageViews() } if (message.isRead) { messageDetailsHeaderView.showMessageAsRead() } else { messageDetailsHeaderView.showMessageAsUnread() } if (isLastItemHeader()) { itemView.lastConversationMessageCollapsedDivider.isVisible = !isMessageBodyExpanded() } } private fun onHeaderCollapsed() { notifyItemChanged(layoutPosition) } private fun isMessageBodyExpanded() = isExpanded(layoutPosition) private fun isLastItemHeader(): Boolean { val lastHeaderItem = visibleItems.last { it.itemType == TYPE_HEADER } return layoutPosition == visibleItems.indexOf(lastHeaderItem) } } inner class ItemViewHolder(view: View) : ExpandableRecyclerAdapter.ViewHolder(view) { fun bind(position: Int, listItem: MessageDetailsListItem.Body, isLastNonDraftItemInTheList: Boolean) { val message = listItem.message Timber.v("Bind item: ${message.messageId}, isDownloaded: ${message.isDownloaded}") val attachmentsView = itemView.attachmentsView attachmentsView.visibility = View.GONE val expirationInfoView = itemView.expirationInfoView val decryptionErrorView = itemView.decryptionErrorView val displayRemoteContentButton = itemView.displayRemoteContentButton val loadEmbeddedImagesButton = itemView.loadEmbeddedImagesButton val openInProtonCalendarView = itemView.include_open_in_proton_calendar val editDraftButton = itemView.editDraftButton val webView = itemView.messageWebViewContainer .findViewById(R.id.item_message_body_web_view_id) ?: return val messageBodyProgress = itemView.messageWebViewContainer .findViewById(R.id.item_message_body_progress_view_id) ?: return message.messageId?.let { setUpWebViewDarkModeBlocking(webView, it) } messageBodyProgress.isVisible = listItem.messageFormattedHtml.isNullOrEmpty() displayRemoteContentButton.isVisible = false loadEmbeddedImagesButton.isVisible = listItem.showLoadEmbeddedImagesButton openInProtonCalendarView.isVisible = listItem.showOpenInProtonCalendar editDraftButton.isVisible = message.isDraft() decryptionErrorView.bind(listItem.showDecryptionError) expirationInfoView.bind(message.expirationTime) setUpSpamScoreView(message.spamScore, itemView.spamScoreView) if (listItem.messageFormattedHtml == null) { Timber.v("Load body for message: ${message.messageId} at position $position, loc: ${message.location}") onLoadMessageBody(message) } listItem.messageFormattedHtml?.let { loadHtmlDataIntoWebView(webView, it) } displayAttachmentInfo(listItem.message.attachments, attachmentsView) setUpViewDividers() val hideMoreActionsButton = listItem.messageFormattedHtml.isNullOrEmpty() || message.isDraft() || !showingMoreThanOneMessage() setupMessageActionsView( message, listItem.messageFormattedHtmlWithQuotedHistory, webView, hideMoreActionsButton ) // TODO: To be decided whether we will need these actions moving forward or they can be removed completely setupReplyActionsView(message, true) setupMessageContentActions( position = position, loadEmbeddedImagesContainer = loadEmbeddedImagesButton, displayRemoteContentButton = displayRemoteContentButton, openInProtonCalenderView = openInProtonCalendarView, editDraftButton = editDraftButton ) setMessageContentHeight(listItem, isLastNonDraftItemInTheList) setHyperlinkCheck(webView, message) } private fun setupMessageActionsView( message: Message, messageHtmlWithQuotedHistory: String?, webView: WebView, shouldHideMoreActionsButton: Boolean ) { val messageActionsView: MessageDetailsActionsView = itemView.messageWebViewContainer.findViewById(R.id.item_message_body_actions_layout_id) ?: return val uiModel = MessageDetailsActionsView.UiModel( hideShowHistory = messageHtmlWithQuotedHistory.isNullOrEmpty(), hideMoreActions = shouldHideMoreActionsButton ) messageActionsView.bind(uiModel) messageActionsView.onShowHistoryClicked { showHistoryButton -> loadHtmlDataIntoWebView(webView, messageHtmlWithQuotedHistory.orEmpty()) showHistoryButton.isVisible = false } messageActionsView.onMoreActionsClicked { onMoreMessageActionsClicked(message) } } private fun setupReplyActionsView( message: Message, shouldHideAllActions: Boolean ) { val replyActionsView: ReplyActionsView = itemView.messageWebViewContainer.findViewById(R.id.item_message_body_reply_actions_layout_id) ?: return replyActionsView.bind( shouldShowReplyAllAction = message.toList.size + message.ccList.size > 1, shouldHideAllActions = shouldHideAllActions ) replyActionsView.onReplyActionClicked { onReplyMessageClicked(Constants.MessageActionType.REPLY, message) } replyActionsView.onReplyAllActionClicked { onReplyMessageClicked(Constants.MessageActionType.REPLY_ALL, message) } replyActionsView.onForwardActionClicked { onReplyMessageClicked(Constants.MessageActionType.FORWARD, message) } } private fun loadHtmlDataIntoWebView(webView: WebView, htmlContent: String) { webView.loadDataWithBaseURL( Constants.DUMMY_URL_PREFIX, htmlContent, "text/html", HTTP.UTF_8, "" ) } private fun setupMessageContentActions( position: Int, loadEmbeddedImagesContainer: Button, displayRemoteContentButton: Button, openInProtonCalenderView: View, editDraftButton: Button ) { loadEmbeddedImagesContainer.setOnClickListener { view -> view.visibility = View.GONE // Once images were loaded for one message, we automatically load them for all the others, so: // the 'load embedded images' button will be hidden for all messages // the 'formatted html' gets reset so that messages which were already rendered without images // go through the rendering again (through `onLoadMessageBody` callback) and load them allItems.map { item -> when (item) { is MessageDetailsListItem.Header -> item is MessageDetailsListItem.Body -> item.copy( showLoadEmbeddedImagesButton = false, showDecryptionError = false, messageFormattedHtml = null ) } } val item = visibleItems[position] as MessageDetailsListItem.Body onLoadEmbeddedImagesClicked(item.message, item.embeddedImageIds) } displayRemoteContentButton.setOnClickListener { val item = visibleItems[position] val webView = itemView.messageWebViewContainer.findViewById(R.id.item_message_body_web_view_id) if (webView != null && webView.contentHeight > 0) { itemView.displayRemoteContentButton.isVisible = false webView.getWebViewClientOrNull()?.allowLoadingRemoteResources() webView.reload() webView.invalidate() onDisplayRemoteContentClicked(item.message) } } openInProtonCalenderView.setOnClickListener { val item = visibleItems[position] onOpenInProtonCalendarClicked(item.message) } editDraftButton.setOnClickListener { val item = visibleItems[position] onEditDraftClicked(item.message) } } private fun setUpViewDividers() { val hideHeaderDivider = itemView.attachmentsView.visibility == View.GONE && itemView.expirationInfoView.visibility == View.VISIBLE itemView.headerDividerView.isVisible = !hideHeaderDivider val showAttachmentsDivider = itemView.attachmentsView.visibility == View.VISIBLE && itemView.expirationInfoView.visibility != View.VISIBLE itemView.attachmentsDividerView.isVisible = showAttachmentsDivider } private fun setMessageContentHeight( listItem: MessageDetailsListItem.Body, isLastNonDraftItemInTheList: Boolean ) { when { !listItem.messageFormattedHtml.isNullOrEmpty() -> { // We want to wrap the message content of the last non-draft item in the list only when the content // has been loaded, to make the message header stay attached to the top of the list // (if we wrap too quickly, the list will scroll down) if (isLastNonDraftItemInTheList) { wrapMessageContentHeightWhenContentLoaded(itemView.messageWebViewContainer) } else { wrapMessageContentHeight(itemView.messageWebViewContainer) } } // For messages in the middle of a conversation, that are not initially expanded when opening // a conversation, we use a fixed height while loading !isLastNonDraftItemInTheList -> { setMessageContentFixedLoadingHeight(itemView.messageWebViewContainer) } } } private fun setHyperlinkCheck(webView: WebView, message: Message) { webView.getWebViewClientOrNull()?.setPhishingCheck(message.isPhishing()) } private fun WebView.getWebViewClientOrNull(): MessageDetailsPmWebViewClient? { return if (WebViewFeature.isFeatureSupported(WebViewFeature.GET_WEB_VIEW_CLIENT)) { WebViewCompat.getWebViewClient(this) as MessageDetailsPmWebViewClient } else { null } } } }