proton-mail-android/app/src/main/java/ch/protonmail/android/activities/messageDetails/MessageDetailsAdapter.kt

795 lines
34 KiB
Kotlin
Raw Normal View History

2020-04-16 15:44:53 +00:00
/*
2022-02-28 15:15:59 +00:00
* Copyright (c) 2022 Proton AG
*
2022-02-28 15:15:59 +00:00
* This file is part of Proton Mail.
*
2022-02-28 15:15:59 +00:00
* Proton Mail is free software: you can redistribute it and/or modify
2020-04-16 15:44:53 +00:00
* 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.
*
2022-02-28 15:15:59 +00:00
* Proton Mail is distributed in the hope that it will be useful,
2020-04-16 15:44:53 +00:00
* 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.
*
2020-04-16 15:44:53 +00:00
* You should have received a copy of the GNU General Public License
2022-02-28 15:15:59 +00:00
* along with Proton Mail. If not, see https://www.gnu.org/licenses/.
2020-04-16 15:44:53 +00:00
*/
package ch.protonmail.android.activities.messageDetails
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.os.Build
2020-04-16 15:44:53 +00:00
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
2020-04-16 15:44:53 +00:00
import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.Button
2020-04-16 15:44:53 +00:00
import android.widget.LinearLayout
import android.widget.ProgressBar
2020-04-16 15:44:53 +00:00
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.core.view.postDelayed
2020-04-16 15:44:53 +00:00
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
2020-04-16 15:44:53 +00:00
import ch.protonmail.android.R
import ch.protonmail.android.activities.messageDetails.attachments.MessageDetailsAttachmentListAdapter
2020-04-16 15:44:53 +00:00
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
Replace imports for import ch.protonmail.android.api.models.room..+; MAILAND-1189 # Conflicts: # app/src/androidTest/java/ch/protonmail/android/api/models/room/ContactGroupsDatabaseTest.kt # app/src/main/java/ch/protonmail/android/api/segments/contact/ContactEmailsManager.kt # app/src/main/java/ch/protonmail/android/api/segments/event/EventHandler.kt # app/src/main/java/ch/protonmail/android/contacts/groups/details/ContactGroupDetailsViewModel.kt # app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsFragment.kt # app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsRepository.kt # app/src/main/java/ch/protonmail/android/contacts/groups/list/ContactGroupsViewModel.kt # app/src/main/java/ch/protonmail/android/contacts/list/listView/ProtonMailContactsLiveData.kt # app/src/main/java/ch/protonmail/android/jobs/UpdateContactJob.java # app/src/test/java/ch/protonmail/android/contacts/details/ContactDetailsRepositoryTest.kt # app/src/test/java/ch/protonmail/android/contacts/details/ContactGroupsRepositoryTest.kt # app/src/test/java/ch/protonmail/android/contacts/groups/ContactGroupsViewModelTest.kt # app/src/test/java/ch/protonmail/android/contacts/groups/edit/ContactGroupEditCreateRepositoryTest.kt # Conflicts: # app/src/main/java/ch/protonmail/android/activities/messageDetails/viewmodel/MessageDetailsViewModel.kt # app/src/main/java/ch/protonmail/android/api/models/messages/receive/ServerAttachment.kt # app/src/main/java/ch/protonmail/android/attachments/DownloadEmbeddedAttachmentsWorker.kt # app/src/main/java/ch/protonmail/android/jobs/helper/EmbeddedImage.kt # app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java # Conflicts: # app/src/androidTest/java/ch/protonmail/android/api/models/room/contacts/ContactDaoTest.kt # Conflicts: # app/src/main/java/ch/protonmail/android/api/models/factories/PackageFactory.java # app/src/main/java/ch/protonmail/android/api/models/factories/SendPreferencesFactory.java # app/src/main/java/ch/protonmail/android/attachments/UploadAttachments.kt # app/src/main/java/ch/protonmail/android/compose/ComposeMessageViewModel.kt # app/src/main/java/ch/protonmail/android/core/ProtonMailApplication.java # app/src/main/java/ch/protonmail/android/jobs/messages/PostMessageJob.java # app/src/main/java/ch/protonmail/android/usecase/compose/SaveDraft.kt # app/src/test/java/ch/protonmail/android/attachments/UploadAttachmentsTest.kt # app/src/test/java/ch/protonmail/android/compose/ComposeMessageViewModelTest.kt # app/src/test/java/ch/protonmail/android/usecase/compose/SaveDraftTest.kt # Conflicts: # app/src/main/java/ch/protonmail/android/di/ApplicationModule.kt # app/src/main/java/ch/protonmail/android/fcm/FcmIntentService.java
2021-03-18 09:46:25 +00:00
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
2020-04-16 15:44:53 +00:00
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
2020-04-16 15:44:53 +00:00
/**
* 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<Message>,
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<String>) -> 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<MessageDetailsListItem>(context) {
2020-04-16 15:44:53 +00:00
private var exclusiveLabelsPerMessage: HashMap<String, List<Label>> = hashMapOf()
private var nonExclusiveLabelsPerMessage: HashMap<String, List<LabelChipUiModel>> = hashMapOf()
2020-04-16 15:44:53 +00:00
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()
}
2020-04-16 15:44:53 +00:00
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
)
2020-04-16 15:44:53 +00:00
}
}
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)
2020-04-16 15:44:53 +00:00
}
}
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<Attachment>,
embeddedImageIds: List<String>,
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<MessageDetailsListItem>()
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<MessageDetailsListItem.Body>()
.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<Attachment>?,
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<WebView>(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<WebView>(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<MessageDetailsListItem>.HeaderViewHolder(view) {
2020-04-16 15:44:53 +00:00
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<MessageDetailsListItem>.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<WebView>(R.id.item_message_body_web_view_id) ?: return
val messageBodyProgress = itemView.messageWebViewContainer
.findViewById<ProgressBar>(R.id.item_message_body_progress_view_id) ?: return
2020-04-16 15:44:53 +00:00
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)
2020-04-16 15:44:53 +00:00
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)
2020-04-16 15:44:53 +00:00
}
displayRemoteContentButton.setOnClickListener {
val item = visibleItems[position]
val webView = itemView.messageWebViewContainer.findViewById<WebView>(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)
2020-04-16 15:44:53 +00:00
}
}
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
2020-04-16 15:44:53 +00:00
}
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
}
}
2020-04-16 15:44:53 +00:00
}
}