Render images in replies
Change-Id: Ia0678184c42a2a01f2e1e65ccd8287bbd71c8c80
This commit is contained in:
parent
048e1edb54
commit
8e97b7c79d
|
@ -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>
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue