Render images in replies

Change-Id: Ia0678184c42a2a01f2e1e65ccd8287bbd71c8c80
This commit is contained in:
SpiritCroc 2022-12-04 12:45:50 +01:00
parent 048e1edb54
commit 8e97b7c79d
7 changed files with 121 additions and 7 deletions

View File

@ -43,4 +43,7 @@
<dimen name="file_icon_size">32dp</dimen> <dimen name="file_icon_size">32dp</dimen>
<dimen name="reply_thumbnail_height">120dp</dimen>
<dimen name="reply_thumbnail_max_width">840dp</dimen>
</resources> </resources>

View File

@ -70,6 +70,7 @@ import im.vector.app.features.html.SpanUtils
import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.html.VectorHtmlCompressor
import im.vector.app.features.location.live.StopLiveLocationShareUseCase import im.vector.app.features.location.live.StopLiveLocationShareUseCase
import im.vector.app.features.location.live.tracking.LocationSharingServiceConnection import im.vector.app.features.location.live.tracking.LocationSharingServiceConnection
import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.raw.wellknown.CryptoConfig import im.vector.app.features.raw.wellknown.CryptoConfig
@ -173,6 +174,7 @@ class TimelineViewModel @AssistedInject constructor(
htmlCompressor: VectorHtmlCompressor, htmlCompressor: VectorHtmlCompressor,
htmlRenderer: EventHtmlRenderer, htmlRenderer: EventHtmlRenderer,
spanUtils: SpanUtils, spanUtils: SpanUtils,
imageContentRenderer: ImageContentRenderer,
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback, Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback,
ReplyPreviewRetriever.PowerLevelProvider, ReplyPreviewRetriever.PreviewReplyRetrieverCallback { ReplyPreviewRetriever.PowerLevelProvider, ReplyPreviewRetriever.PreviewReplyRetrieverCallback {
@ -199,7 +201,8 @@ class TimelineViewModel @AssistedInject constructor(
messageColorProvider, messageColorProvider,
htmlCompressor, htmlCompressor,
htmlRenderer, htmlRenderer,
spanUtils spanUtils,
imageContentRenderer,
) )
override fun resolveDisplayName(senderInfo: SenderInfo): String = (timeline?.senderWithLiveRoomState(senderInfo) ?: senderInfo).disambiguatedDisplayName override fun resolveDisplayName(senderInfo: SenderInfo): String = (timeline?.senderWithLiveRoomState(senderInfo) ?: senderInfo).disambiguatedDisplayName

View File

@ -273,7 +273,15 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder>(
override fun onStateUpdated(state: PreviewReplyUiState) { override fun onStateUpdated(state: PreviewReplyUiState) {
replyPreviewRetriever?.let { replyPreviewRetriever?.let {
replyView?.render(state, it, attributes.informationData, movementMethod, attributes.itemLongClickListener, coroutineScope) replyView?.render(
state,
it,
attributes.informationData,
movementMethod,
attributes.itemLongClickListener,
coroutineScope,
attributes.generateMissingVideoThumbnails
)
} }
} }
} }

View File

@ -33,7 +33,9 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.text.PrecomputedTextCompat import androidx.core.text.PrecomputedTextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.TextViewCompat import androidx.core.widget.TextViewCompat
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.extensions.tintBackground import im.vector.app.core.extensions.tintBackground
import im.vector.app.databinding.ViewInReplyToBinding import im.vector.app.databinding.ViewInReplyToBinding
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
@ -41,10 +43,18 @@ import im.vector.app.features.home.room.detail.timeline.item.BindingOptions
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess
import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.getCaption
import org.matrix.android.sdk.api.session.room.model.message.getFileName
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import timber.log.Timber import timber.log.Timber
@ -68,6 +78,9 @@ class InReplyToView @JvmOverloads constructor(
private var state: PreviewReplyUiState = PreviewReplyUiState.NoReply private var state: PreviewReplyUiState = PreviewReplyUiState.NoReply
private val maxThumbnailWidth = context.resources.getDimensionPixelSize(R.dimen.reply_thumbnail_max_width)
private val maxThumbnailHeight = context.resources.getDimensionPixelSize(R.dimen.reply_thumbnail_height)
/** /**
* This methods is responsible for rendering the view according to the newState * This methods is responsible for rendering the view according to the newState
* *
@ -79,7 +92,9 @@ class InReplyToView @JvmOverloads constructor(
movementMethod: MovementMethod?, movementMethod: MovementMethod?,
itemLongClickListener: OnLongClickListener?, itemLongClickListener: OnLongClickListener?,
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
force: Boolean = false) { generateMissingVideoThumbnails: Boolean,
force: Boolean = false
) {
if (newState == state && !force) { if (newState == state && !force) {
return return
} }
@ -90,7 +105,14 @@ class InReplyToView @JvmOverloads constructor(
PreviewReplyUiState.NoReply -> renderHidden() PreviewReplyUiState.NoReply -> renderHidden()
is PreviewReplyUiState.ReplyLoading -> renderLoading() is PreviewReplyUiState.ReplyLoading -> renderLoading()
is PreviewReplyUiState.Error -> renderError(newState) is PreviewReplyUiState.Error -> renderError(newState)
is PreviewReplyUiState.InReplyTo -> renderReplyTo(newState, retriever, roomInformationData, movementMethod, coroutineScope) is PreviewReplyUiState.InReplyTo -> renderReplyTo(
newState,
retriever,
roomInformationData,
movementMethod,
coroutineScope,
generateMissingVideoThumbnails
)
} }
setOnLongClickListener(itemLongClickListener) setOnLongClickListener(itemLongClickListener)
@ -117,6 +139,7 @@ class InReplyToView @JvmOverloads constructor(
private fun hideViews() { private fun hideViews() {
views.replyMemberNameView.isVisible = false views.replyMemberNameView.isVisible = false
views.replyTextView.isVisible = false views.replyTextView.isVisible = false
views.replyThumbnailView.isVisible = false
renderFadeOut(null) renderFadeOut(null)
} }
@ -154,7 +177,8 @@ class InReplyToView @JvmOverloads constructor(
retriever: ReplyPreviewRetriever, retriever: ReplyPreviewRetriever,
roomInformationData: MessageInformationData, roomInformationData: MessageInformationData,
movementMethod: MovementMethod?, movementMethod: MovementMethod?,
coroutineScope: CoroutineScope coroutineScope: CoroutineScope,
generateMissingVideoThumbnails: Boolean,
) { ) {
hideViews() hideViews()
isVisible = true isVisible = true
@ -169,6 +193,8 @@ class InReplyToView @JvmOverloads constructor(
renderFadeOut(roomInformationData) renderFadeOut(roomInformationData)
when (val content = state.event.getLastMessageContent()) { when (val content = state.event.getLastMessageContent()) {
is MessageTextContent -> renderTextContent(content, retriever, movementMethod, coroutineScope) is MessageTextContent -> renderTextContent(content, retriever, movementMethod, coroutineScope)
is MessageImageInfoContent -> renderImageThumbnailContent(content, state.event, retriever)
is MessageVideoContent -> renderVideoThumbnailContent(content, state.event, retriever, generateMissingVideoThumbnails)
else -> renderFallback(state.event, retriever) else -> renderFallback(state.event, retriever)
} }
} }
@ -221,6 +247,68 @@ class InReplyToView @JvmOverloads constructor(
markwonPlugins.forEach { plugin -> plugin.afterSetText(views.replyTextView) } markwonPlugins.forEach { plugin -> plugin.afterSetText(views.replyTextView) }
} }
private fun renderImageThumbnailContent(
content: MessageImageInfoContent,
event: TimelineEvent,
retriever: ReplyPreviewRetriever,
) {
val data = ImageContentRenderer.Data(
eventId = event.eventId,
filename = content.getFileName(),
caption = content.getCaption(),
mimeType = content.mimeType,
url = content.getFileUrl(),
elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
height = content.info?.height,
maxHeight = maxThumbnailHeight,
width = content.info?.width,
maxWidth = maxThumbnailWidth,
allowNonMxcUrls = false
)
renderThumbnailContent(data, retriever)
}
private fun renderVideoThumbnailContent(
content: MessageVideoContent,
event: TimelineEvent,
retriever: ReplyPreviewRetriever,
generateMissingVideoThumbnails: Boolean,
) {
val thumbnailData = ImageContentRenderer.Data(
eventId = event.eventId,
filename = content.getFileName(),
caption = content.getCaption(),
mimeType = content.mimeType,
url = content.videoInfo?.getThumbnailUrl(),
elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = content.videoInfo?.height,
maxHeight = maxThumbnailHeight,
width = content.videoInfo?.width,
maxWidth = maxThumbnailWidth,
allowNonMxcUrls = false,
// Video fallback for generating thumbnails
downloadFallbackIfThumbnailMissing = generateMissingVideoThumbnails,
fallbackUrl = content.getFileUrl(),
fallbackElementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt()
)
renderThumbnailContent(thumbnailData, retriever)
}
private fun renderThumbnailContent(
mediaData: ImageContentRenderer.Data,
retriever: ReplyPreviewRetriever,
) {
views.replyThumbnailView.isVisible = true
retriever.imageContentRenderer.render(
mediaData,
ImageContentRenderer.Mode.THUMBNAIL,
views.replyThumbnailView,
animate = false
)
views.replyTextView.setTextOrHide(mediaData.caption)
}
private fun renderFallback(event: TimelineEvent, retriever: ReplyPreviewRetriever) { private fun renderFallback(event: TimelineEvent, retriever: ReplyPreviewRetriever) {
views.replyTextView.isVisible = true views.replyTextView.isVisible = true
views.replyTextView.text = retriever.formatFallbackReply(event) views.replyTextView.text = retriever.formatFallbackReply(event)

View File

@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.reply
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory
import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
@ -28,6 +27,7 @@ import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.html.PillsPostProcessor
import im.vector.app.features.html.SpanUtils import im.vector.app.features.html.SpanUtils
import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.html.VectorHtmlCompressor
import im.vector.app.features.media.ImageContentRenderer
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -42,7 +42,6 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.room.sender.SenderInfo
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLatestEventId import org.matrix.android.sdk.api.session.room.timeline.getLatestEventId
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
@ -60,6 +59,7 @@ class ReplyPreviewRetriever(
val htmlCompressor: VectorHtmlCompressor, val htmlCompressor: VectorHtmlCompressor,
val htmlRenderer: EventHtmlRenderer, val htmlRenderer: EventHtmlRenderer,
val spanUtils: SpanUtils, val spanUtils: SpanUtils,
val imageContentRenderer: ImageContentRenderer,
) { ) {
private data class ReplyPreviewUiState( private data class ReplyPreviewUiState(
// Id of the latest event in the case of an edited event, or the eventId for an event which has not been edited // Id of the latest event in the case of an edited event, or the eventId for an event which has not been edited

View File

@ -31,6 +31,18 @@
android:textSize="12sp" android:textSize="12sp"
tools:text="@sample/users.json/data/displayName" /> tools:text="@sample/users.json/data/displayName" />
<ImageView
android:id="@+id/replyThumbnailView"
android:layout_width="0dp"
android:layout_height="@dimen/reply_thumbnail_height"
android:contentDescription="@string/a11y_image"
android:layout_marginTop="8dp"
android:layout_gravity="start"
android:scaleType="fitStart"
tools:layout_width="200dp"
app:layout_goneMarginTop="0dp"
tools:src="@tools:sample/backgrounds/scenic" />
<com.ruesga.rview.widget.ExpandableViewLayout <com.ruesga.rview.widget.ExpandableViewLayout
android:id="@+id/expandableReplyView" android:id="@+id/expandableReplyView"
android:layout_width="wrap_content" android:layout_width="wrap_content"