Display embedded images for each message in a conversation

after they were loaded through the "Display embedded images" button for
any message.

There were some issues preventing the functionality from working which
were addressed in this PR:
- The same instance of MessageRenderer was not ready to work with
different messages (messageBody was not changeable and the is a check
that would stop execution when an image was "already rendered" which
didn't take account that different messages can contain the same image)
- When MessageRenderer returns the html, we need to display that
  through a new livedata which targets only the message currently opened,
  as resetting the whole ConversationUiModel causes UI glitches when the
  message is not the last one.

MAILAND-1931
This commit is contained in:
Marino Meneghel 2021-06-17 15:05:07 +02:00
parent 8ad3552fa1
commit 3cf1ac163d
4 changed files with 31 additions and 26 deletions

View File

@ -68,7 +68,7 @@ internal class MessageDetailsAdapter(
private val onLoadEmbeddedImagesClicked: ((Message) -> Unit)?,
private val onDisplayRemoteContentClicked: ((Message) -> Unit)?,
private val userManager: UserManager,
private val onLoadMessage: (Message) -> Unit,
private val onLoadMessageBody: (Message) -> Unit,
private val onAttachmentDownloadCallback: (Attachment) -> Unit
) : ExpandableRecyclerAdapter<MessageDetailsAdapter.MessageDetailsListItem>(context) {
@ -179,7 +179,7 @@ internal class MessageDetailsAdapter(
Timber.v("Load data for message: ${message.messageId} at position $position")
if (listItem.messageFormattedHtml == null) {
onLoadMessage(message)
onLoadMessageBody(message)
}
val webView = itemView.messageWebViewContainer.getChildAt(0) as? WebView ?: return

View File

@ -68,16 +68,12 @@ internal class MessageRenderer(
/** The [String] html of the message body */
var messageBody: String? = null
set(value) {
// Return if body is already set
if (field != null) return
// Update if value is not null
if (value != null) field = value
field = value
// Clear inlined images to ensure when messageBody changes the loading of images doesn't get blocked
// (messageBody changing means we're loading images for another message in the same conversation)
inlinedImageIds.clear()
}
/** reference to the [Document] */
private val document by lazy { documentParser(messageBody!!) }
/** A [Channel] for receive new [EmbeddedImage] images to inline in [document] */
val images = actor<List<EmbeddedImage>> {
for (embeddedImages in channel) {
@ -175,6 +171,10 @@ internal class MessageRenderer(
private val imageInliner = actor<List<ImageString>> {
for (imageStrings in channel) {
// Document is parsed for each emission because `messageBody`
// field can change when switching messages in a conversation
val document = documentParser(messageBody!!)
for (imageString in imageStrings) {
val (embeddedImage, image64) = imageString
@ -194,14 +194,8 @@ internal class MessageRenderer(
// Extract the message ID for which embedded images are being loaded
// to pass it back to the caller along with the rendered body
val messageId = imageStrings.firstOrNull()?.first?.messageId ?: continue
documentStringifier.send(messageId)
}
}
/** Actor that will stringify the [document] */
private val documentStringifier = actor<String> {
for (messageId in channel)
renderedMessage.send(RenderedMessage(messageId, document.toString()))
}
}
/** `CoroutineContext` for [idsListUpdater] for update [inlinedImageIds] of a single thread */
@ -229,7 +223,7 @@ internal class MessageRenderer(
private val imageDecoder: ImageDecoder = DefaultImageDecoder()
) {
/** @return new instance of [MessageRenderer] with the given [messageBody] */
/** @return new instance of [MessageRenderer] */
fun create(scope: CoroutineScope) =
MessageRenderer(dispatchers, documentParser, imageDecoder, attachmentsDirectory, scope)
}

View File

@ -183,6 +183,7 @@ internal class MessageDetailsViewModel @Inject constructor(
private val _prepareEditMessageIntentResult: MutableLiveData<Event<IntentExtrasData>> = MutableLiveData()
private val _decryptedConversationUiModel: MutableLiveData<ConversationUiModel> = MutableLiveData()
private val _messageRenderedWithImages: MutableLiveData<Message> = MutableLiveData()
private val _checkStoragePermission: MutableLiveData<Event<Boolean>> = MutableLiveData()
private val _messageDetailsError: MutableLiveData<Event<String>> = MutableLiveData()
@ -222,6 +223,9 @@ internal class MessageDetailsViewModel @Inject constructor(
val decryptedConversationUiModel: LiveData<ConversationUiModel>
get() = _decryptedConversationUiModel
val messageRenderedWithImages: LiveData<Message>
get() = _messageRenderedWithImages
private var areImagesDisplayed: Boolean = false
init {
@ -230,7 +234,7 @@ internal class MessageDetailsViewModel @Inject constructor(
val currentUiModel = _decryptedConversationUiModel.value
val message = currentUiModel?.messages?.find { it.messageId == renderedMessage.messageId }
message?.decryptedHTML = renderedMessage.renderedHtmlBody
_decryptedConversationUiModel.value = currentUiModel
_messageRenderedWithImages.value = message
areImagesDisplayed = true
}
}

View File

@ -190,6 +190,15 @@ internal class MessageDetailsActivity : BaseStoragePermissionActivity() {
private fun continueSetup() {
viewModel.conversationUiModel.observe(this) { viewModel.loadMailboxItemDetails() }
viewModel.decryptedConversationUiModel.observe(this, ConversationUiModelObserver())
viewModel.messageRenderedWithImages.observe(this) { message ->
val messageId = message.messageId ?: return@observe
messageExpandableAdapter.showMessageDetails(
message.decryptedHTML,
messageId,
false,
message.attachments
)
}
viewModel.labels
.onEach(messageExpandableAdapter::setAllLabels)
@ -463,15 +472,15 @@ internal class MessageDetailsActivity : BaseStoragePermissionActivity() {
private inner class ConversationUiModelObserver : Observer<ConversationUiModel> {
override fun onChanged(conversation: ConversationUiModel) {
val message = lastMessage(conversation)
val lastMessage = conversation.messages.last()
displayToolbarData(conversation)
setupLastMessageActionsListener(message)
setupLastMessageActionsListener(lastMessage)
Timber.v("New decrypted message ${message.messageId}")
Timber.v("New decrypted message ${lastMessage.messageId}")
viewModel.renderedFromCache = AtomicBoolean(true)
val decryptedBody = getDecryptedBody(message.decryptedHTML)
if (decryptedBody.isEmpty() || message.messageBody.isNullOrEmpty()) {
val decryptedBody = getDecryptedBody(lastMessage.decryptedHTML)
if (decryptedBody.isEmpty() || lastMessage.messageBody.isNullOrEmpty()) {
UiUtil.showInfoSnack(mSnackLayout, this@MessageDetailsActivity, R.string.decryption_error_desc).show()
return
}
@ -548,8 +557,6 @@ internal class MessageDetailsActivity : BaseStoragePermissionActivity() {
expandedToolbarTitleTextView.text = subject
}
private fun lastMessage(conversation: ConversationUiModel): Message = conversation.messages.last()
fun executeMessageAction(
messageAction: Constants.MessageActionType,
message: Message = requireNotNull(viewModel.decryptedConversationUiModel.value?.messages?.last())