Render MSC2530 captions

Change-Id: I10f875121e90102a0518d9bd39d87b3daa68ef2e
This commit is contained in:
SpiritCroc 2022-11-10 10:38:04 +01:00
parent 3214c782bc
commit 58dd1dedc9
21 changed files with 192 additions and 23 deletions

View file

@ -32,6 +32,7 @@ Here you can find some extra features and changes compared to Element Android (w
- Render inline images / custom emojis in the timeline - Render inline images / custom emojis in the timeline
- Render image reactions - Render image reactions
- Send freeform reactions - Send freeform reactions
- Render media captions ([MSC2530](https://github.com/matrix-org/matrix-spec-proposals/pull/2530))
- Branding (name, app icon, links) - Branding (name, app icon, links)
- Show a toast instead of a snackbar after copying text, in order to not block the input area right after copying - Show a toast instead of a snackbar after copying text, in order to not block the input area right after copying

View file

@ -45,6 +45,11 @@ data class MessageAudioContent(
*/ */
@Json(name = "url") override val url: String? = null, @Json(name = "url") override val url: String? = null,
/**
* MSC 2530: filename as filename, using body as caption instead
*/
@Json(name = "filename") override val filename: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null, @Json(name = "m.new_content") override val newContent: Content? = null,

View file

@ -38,7 +38,7 @@ data class MessageFileContent(
/** /**
* The original filename of the uploaded file. * The original filename of the uploaded file.
*/ */
@Json(name = "filename") val filename: String? = null, @Json(name = "filename") override val filename: String? = null,
/** /**
* Information about the file referred to in url. * Information about the file referred to in url.

View file

@ -45,6 +45,11 @@ data class MessageImageContent(
*/ */
@Json(name = "url") override val url: String? = null, @Json(name = "url") override val url: String? = null,
/**
* MSC 2530: filename as filename, using body as caption instead
*/
@Json(name = "filename") override val filename: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null, @Json(name = "m.new_content") override val newContent: Content? = null,

View file

@ -46,6 +46,11 @@ data class MessageStickerContent(
*/ */
@Json(name = "url") override val url: String? = null, @Json(name = "url") override val url: String? = null,
/**
* MSC 2530: filename as filename, using body as caption instead
*/
@Json(name = "filename") override val filename: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null, @Json(name = "m.new_content") override val newContent: Content? = null,

View file

@ -44,6 +44,11 @@ data class MessageVideoContent(
*/ */
@Json(name = "url") override val url: String? = null, @Json(name = "url") override val url: String? = null,
/**
* MSC 2530: filename as filename, using body as caption instead
*/
@Json(name = "filename") override val filename: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null, @Json(name = "m.new_content") override val newContent: Content? = null,

View file

@ -33,6 +33,9 @@ interface MessageWithAttachmentContent : MessageContent {
val encryptedFileInfo: EncryptedFileInfo? val encryptedFileInfo: EncryptedFileInfo?
val mimeType: String? val mimeType: String?
// MSC 2530 adds filename for other media types than m.file as well
val filename: String?
} }
/** /**
@ -40,4 +43,6 @@ interface MessageWithAttachmentContent : MessageContent {
*/ */
fun MessageWithAttachmentContent.getFileUrl() = encryptedFileInfo?.url ?: url fun MessageWithAttachmentContent.getFileUrl() = encryptedFileInfo?.url ?: url
fun MessageWithAttachmentContent.getFileName() = (this as? MessageFileContent)?.getFileName() ?: body fun MessageWithAttachmentContent.getFileName() = (this as? MessageFileContent)?.getFileName() ?: filename ?: body
fun MessageWithAttachmentContent.getCaption() = body.takeIf { filename != null && filename != it }

View file

@ -108,6 +108,8 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
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.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
@ -285,7 +287,8 @@ class MessageItemFactory @Inject constructor(
return MessageAudioItem_() return MessageAudioItem_()
.attributes(attributes) .attributes(attributes)
.filename(messageContent.body) .filename(messageContent.getFileName())
.caption(messageContent.getCaption())
.duration(messageContent.audioInfo?.duration ?: 0) .duration(messageContent.audioInfo?.duration ?: 0)
.playbackControlButtonClickListener(playbackControlButtonClickListener) .playbackControlButtonClickListener(playbackControlButtonClickListener)
.audioMessagePlaybackTracker(audioMessagePlaybackTracker) .audioMessagePlaybackTracker(audioMessagePlaybackTracker)
@ -418,7 +421,8 @@ class MessageItemFactory @Inject constructor(
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder) .contentDownloadStateTrackerBinder(contentDownloadStateTrackerBinder)
.highlighted(highlight) .highlighted(highlight)
.filename(messageContent.body) .filename(messageContent.getFileName())
.caption(messageContent.getCaption())
.iconRes(R.drawable.ic_paperclip) .iconRes(R.drawable.ic_paperclip)
} }
@ -456,7 +460,8 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val data = ImageContentRenderer.Data( val data = ImageContentRenderer.Data(
eventId = informationData.eventId, eventId = informationData.eventId,
filename = messageContent.body, filename = messageContent.getFileName(),
caption = messageContent.getCaption(),
mimeType = messageContent.mimeType, mimeType = messageContent.mimeType,
url = messageContent.getFileUrl(), url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
@ -505,7 +510,8 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data( val thumbnailData = ImageContentRenderer.Data(
eventId = informationData.eventId, eventId = informationData.eventId,
filename = messageContent.body, filename = messageContent.getFileName(),
caption = messageContent.getCaption(),
mimeType = messageContent.mimeType, mimeType = messageContent.mimeType,
url = messageContent.videoInfo?.getThumbnailUrl(), url = messageContent.videoInfo?.getThumbnailUrl(),
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
@ -522,7 +528,8 @@ class MessageItemFactory @Inject constructor(
val videoData = VideoContentRenderer.Data( val videoData = VideoContentRenderer.Data(
eventId = informationData.eventId, eventId = informationData.eventId,
filename = messageContent.body, filename = messageContent.getFileName(),
caption = messageContent.getCaption(),
mimeType = messageContent.mimeType, mimeType = messageContent.mimeType,
url = messageContent.getFileUrl(), url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),

View file

@ -23,6 +23,8 @@ import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent 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.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl 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
@ -33,7 +35,8 @@ fun TimelineEvent.buildImageContentRendererData(maxHeight: Int, generateMissingV
?.let { messageImageContent -> ?.let { messageImageContent ->
ImageContentRenderer.Data( ImageContentRenderer.Data(
eventId = eventId, eventId = eventId,
filename = messageImageContent.body, filename = messageImageContent.getFileName(),
caption = messageImageContent.getCaption(),
mimeType = messageImageContent.mimeType, mimeType = messageImageContent.mimeType,
url = messageImageContent.getFileUrl(), url = messageImageContent.getFileUrl(),
elementToDecrypt = messageImageContent.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = messageImageContent.encryptedFileInfo?.toElementToDecrypt(),
@ -49,7 +52,8 @@ fun TimelineEvent.buildImageContentRendererData(maxHeight: Int, generateMissingV
val videoInfo = messageVideoContent.videoInfo val videoInfo = messageVideoContent.videoInfo
ImageContentRenderer.Data( ImageContentRenderer.Data(
eventId = eventId, eventId = eventId,
filename = messageVideoContent.body, filename = messageVideoContent.getFileName(),
caption = messageVideoContent.getCaption(),
mimeType = videoInfo?.thumbnailInfo?.mimeType, mimeType = videoInfo?.thumbnailInfo?.mimeType,
url = videoInfo?.getThumbnailUrl(), url = videoInfo?.getThumbnailUrl(),
elementToDecrypt = videoInfo?.thumbnailFile?.toElementToDecrypt(), elementToDecrypt = videoInfo?.thumbnailFile?.toElementToDecrypt(),

View file

@ -30,11 +30,14 @@ import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.ui.views.FooteredTextView
import im.vector.app.core.utils.TextUtils import im.vector.app.core.utils.TextUtils
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
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.view.ScMessageBubbleWrapView
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
@EpoxyModelClass @EpoxyModelClass
@ -43,6 +46,9 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var filename: String = "" var filename: String = ""
@EpoxyAttribute
var caption: String? = null
@EpoxyAttribute @EpoxyAttribute
var mxcUrl: String = "" var mxcUrl: String = ""
@ -115,6 +121,7 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
holder.fileSize.text = holder.rootLayout.context.getString( holder.fileSize.text = holder.rootLayout.context.getString(
R.string.audio_message_file_size, formattedFileSize R.string.audio_message_file_size, formattedFileSize
) )
holder.captionView.setTextOrHide(caption)
holder.mainLayout.contentDescription = holder.rootLayout.context.getString( holder.mainLayout.contentDescription = holder.rootLayout.context.getString(
R.string.a11y_audio_message_item, filename, durationContentDescription, formattedFileSize R.string.a11y_audio_message_item, filename, durationContentDescription, formattedFileSize
) )
@ -195,6 +202,19 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
audioMessagePlaybackTracker.untrack(attributes.informationData.eventId) audioMessagePlaybackTracker.untrack(attributes.informationData.eventId)
} }
private fun hasCaption() = !caption.isNullOrBlank()
override fun allowFooterOverlay(holder: Holder, bubbleWrapView: ScMessageBubbleWrapView): Boolean = hasCaption()
override fun needsFooterReservation(): Boolean {
return hasCaption()
}
override fun reserveFooterSpace(holder: Holder, width: Int, height: Int) {
holder.captionView.footerWidth = width
holder.captionView.footerHeight = height
}
override fun getViewStubId() = STUB_ID override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) { class Holder : AbsMessageItem.Holder(STUB_ID) {
@ -205,6 +225,7 @@ abstract class MessageAudioItem : AbsMessageItem<MessageAudioItem.Holder>() {
val audioPlaybackTime by bind<TextView>(R.id.audioPlaybackTime) val audioPlaybackTime by bind<TextView>(R.id.audioPlaybackTime)
val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout) val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout)
val fileSize by bind<TextView>(R.id.fileSize) val fileSize by bind<TextView>(R.id.fileSize)
val captionView by bind<FooteredTextView>(R.id.messageCaptionView)
val audioPlaybackDuration by bind<TextView>(R.id.audioPlaybackDuration) val audioPlaybackDuration by bind<TextView>(R.id.audioPlaybackDuration)
val audioSeekBar by bind<SeekBar>(R.id.audioSeekBar) val audioSeekBar by bind<SeekBar>(R.id.audioSeekBar)
} }

View file

@ -29,9 +29,12 @@ import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.ui.views.FooteredTextView
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
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.view.ScMessageBubbleWrapView
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.themes.guessTextWidth import im.vector.app.features.themes.guessTextWidth
import kotlin.math.ceil import kotlin.math.ceil
@ -43,6 +46,9 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var filename: String = "" var filename: String = ""
@EpoxyAttribute
var caption: String? = null
@EpoxyAttribute @EpoxyAttribute
var mxcUrl: String = "" var mxcUrl: String = ""
@ -74,6 +80,7 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
} }
holder.filenameView.text = filename holder.filenameView.text = filename
holder.captionView.setTextOrHide(caption)
if (attributes.informationData.sendState.isSending()) { if (attributes.informationData.sendState.isSending()) {
holder.fileImageView.setImageResource(iconRes) holder.fileImageView.setImageResource(iconRes)
@ -124,6 +131,19 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
holder.mainLayout.setPadding(0, 0, 0, 0) holder.mainLayout.setPadding(0, 0, 0, 0)
} }
private fun hasCaption() = !caption.isNullOrBlank()
override fun allowFooterOverlay(holder: Holder, bubbleWrapView: ScMessageBubbleWrapView): Boolean = hasCaption()
override fun needsFooterReservation(): Boolean {
return hasCaption()
}
override fun reserveFooterSpace(holder: Holder, width: Int, height: Int) {
holder.captionView.footerWidth = width
holder.captionView.footerHeight = height
}
override fun getViewStubId() = STUB_ID override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) { class Holder : AbsMessageItem.Holder(STUB_ID) {
@ -134,6 +154,7 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
val fileImageWrapper by bind<ViewGroup>(R.id.messageFileImageView) val fileImageWrapper by bind<ViewGroup>(R.id.messageFileImageView)
val fileDownloadProgress by bind<ProgressBar>(R.id.messageFileProgressbar) val fileDownloadProgress by bind<ProgressBar>(R.id.messageFileProgressbar)
val filenameView by bind<TextView>(R.id.messageFilenameView) val filenameView by bind<TextView>(R.id.messageFilenameView)
val captionView by bind<FooteredTextView>(R.id.messageCaptionView)
} }
companion object { companion object {

View file

@ -31,8 +31,10 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.files.LocalFilesHelper import im.vector.app.core.files.LocalFilesHelper
import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideApp
import im.vector.app.core.ui.views.FooteredTextView
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
@ -130,6 +132,8 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
holder.mediaContentView.onClick(attributes.itemClickListener) holder.mediaContentView.onClick(attributes.itemClickListener)
holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener) holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener)
holder.captionView.setTextOrHide(mediaData.caption)
holder.playContentView.visibility = if (animate) { holder.playContentView.visibility = if (animate) {
View.GONE View.GONE
} else if (playable) { } else if (playable) {
@ -139,6 +143,8 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
} }
} }
private fun hasCaption() = !mediaData.caption.isNullOrBlank()
override fun unbind(holder: Holder) { override fun unbind(holder: Holder) {
GlideApp.with(holder.view.context.applicationContext).clear(holder.imageView) GlideApp.with(holder.view.context.applicationContext).clear(holder.imageView)
imageContentRenderer.clear(holder.imageView) imageContentRenderer.clear(holder.imageView)
@ -151,6 +157,9 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
override fun getViewStubId() = STUB_ID override fun getViewStubId() = STUB_ID
private fun shouldAllowFooterOverlay(footerMeasures: Array<Int>, imageWidth: Int, imageHeight: Int): Boolean { private fun shouldAllowFooterOverlay(footerMeasures: Array<Int>, imageWidth: Int, imageHeight: Int): Boolean {
if (hasCaption()) {
return true
}
val footerWidth = footerMeasures[0] val footerWidth = footerMeasures[0]
val footerHeight = footerMeasures[1] val footerHeight = footerMeasures[1]
// We need enough space in both directions to remain within the image bounds. // We need enough space in both directions to remain within the image bounds.
@ -159,6 +168,9 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
} }
private fun shouldShowFooterBellow(footerMeasures: Array<Int>, imageWidth: Int, imageHeight: Int): Boolean { private fun shouldShowFooterBellow(footerMeasures: Array<Int>, imageWidth: Int, imageHeight: Int): Boolean {
if (hasCaption()) {
return false
}
// Only show footer bellow if the width is not the limiting factor (or it will get cut). // Only show footer bellow if the width is not the limiting factor (or it will get cut).
// Otherwise, we can not be sure in this place that we'll have enough space on the side // Otherwise, we can not be sure in this place that we'll have enough space on the side
// Also, prefer footer on the side if possible (i.e. enough height available) // Also, prefer footer on the side if possible (i.e. enough height available)
@ -168,6 +180,9 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
} }
override fun allowFooterOverlay(holder: Holder, bubbleWrapView: ScMessageBubbleWrapView): Boolean { override fun allowFooterOverlay(holder: Holder, bubbleWrapView: ScMessageBubbleWrapView): Boolean {
if (hasCaption()) {
return true
}
val rememberedAllowFooterOverlay = forceAllowFooterOverlay val rememberedAllowFooterOverlay = forceAllowFooterOverlay
if (rememberedAllowFooterOverlay != null) { if (rememberedAllowFooterOverlay != null) {
lastAllowedFooterOverlay = rememberedAllowFooterOverlay lastAllowedFooterOverlay = rememberedAllowFooterOverlay
@ -187,15 +202,30 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
} }
override fun allowFooterBelow(holder: Holder): Boolean { override fun allowFooterBelow(holder: Holder): Boolean {
if (hasCaption()) {
return true
}
val showBellow = showFooterBellow val showBellow = showFooterBellow
lastShowFooterBellow = showBellow lastShowFooterBellow = showBellow
return showBellow return showBellow
} }
override fun getScBubbleMargin(resources: Resources): Int { override fun getScBubbleMargin(resources: Resources): Int {
if (hasCaption()) {
return super.getScBubbleMargin(resources)
}
return 0 return 0
} }
override fun needsFooterReservation(): Boolean {
return hasCaption()
}
override fun reserveFooterSpace(holder: Holder, width: Int, height: Int) {
holder.captionView.footerWidth = width
holder.captionView.footerHeight = height
}
override fun applyScBubbleStyle(messageLayout: TimelineMessageLayout.ScBubble, holder: Holder) { override fun applyScBubbleStyle(messageLayout: TimelineMessageLayout.ScBubble, holder: Holder) {
// Case: ImageContentRenderer.processSize only sees width=height=0 -> width of the ImageView not adapted to the actual content // Case: ImageContentRenderer.processSize only sees width=height=0 -> width of the ImageView not adapted to the actual content
// -> Align image within ImageView to same side as message bubbles // -> Align image within ImageView to same side as message bubbles
@ -208,7 +238,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
when { when {
// Don't show it for non-bubble layouts, don't show for Stickers, ... // Don't show it for non-bubble layouts, don't show for Stickers, ...
// Also only supported for default corner radius // Also only supported for default corner radius
!(messageLayout.isRealBubble || messageLayout.isPseudoBubble) || mode != ImageContentRenderer.Mode.THUMBNAIL -> { !(messageLayout.isRealBubble || messageLayout.isPseudoBubble) || hasCaption() || mode != ImageContentRenderer.Mode.THUMBNAIL -> {
holder.mediaContentView.background = null holder.mediaContentView.background = null
} }
attributes.informationData.sentByMe -> { attributes.informationData.sentByMe -> {
@ -223,6 +253,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
class Holder : AbsMessageItem.Holder(STUB_ID) { class Holder : AbsMessageItem.Holder(STUB_ID) {
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout) val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
val imageView by bind<ImageView>(R.id.messageThumbnailView) val imageView by bind<ImageView>(R.id.messageThumbnailView)
val captionView by bind<FooteredTextView>(R.id.messageCaptionView)
val playContentView by bind<ImageView>(R.id.messageMediaPlayView) val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia) val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia)
} }

View file

@ -31,6 +31,8 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageNoticeContent import org.matrix.android.sdk.api.session.room.model.message.MessageNoticeContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.model.message.getCaption
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 org.matrix.android.sdk.api.session.room.timeline.isEdition import org.matrix.android.sdk.api.session.room.timeline.isEdition
@ -209,6 +211,7 @@ class TimelineMessageLayoutFactory @Inject constructor(
private fun MessageContent?.isPseudoBubble(event: TimelineEvent): Boolean { private fun MessageContent?.isPseudoBubble(event: TimelineEvent): Boolean {
if (this == null) return false if (this == null) return false
if (event.root.isRedacted()) return false if (event.root.isRedacted()) return false
if (this is MessageWithAttachmentContent && !getCaption().isNullOrBlank()) return false
return this.msgType in MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT return this.msgType in MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT
} }

View file

@ -52,6 +52,7 @@ import kotlin.math.min
interface AttachmentData : Parcelable { interface AttachmentData : Parcelable {
val eventId: String val eventId: String
val filename: String val filename: String
val caption: String?
val mimeType: String? val mimeType: String?
val url: String? val url: String?
val elementToDecrypt: ElementToDecrypt? val elementToDecrypt: ElementToDecrypt?
@ -73,6 +74,7 @@ class ImageContentRenderer @Inject constructor(
data class Data( data class Data(
override val eventId: String, override val eventId: String,
override val filename: String, override val filename: String,
override val caption: String?,
override val mimeType: String?, override val mimeType: String?,
override val url: String?, override val url: String?,
override val elementToDecrypt: ElementToDecrypt?, override val elementToDecrypt: ElementToDecrypt?,

View file

@ -29,6 +29,8 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
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.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl 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
@ -60,7 +62,8 @@ class RoomEventsAttachmentProvider(
if (content is MessageImageContent) { if (content is MessageImageContent) {
val data = ImageContentRenderer.Data( val data = ImageContentRenderer.Data(
eventId = it.eventId, eventId = it.eventId,
filename = content.body, filename = content.getFileName(),
caption = content.getCaption(),
mimeType = content.mimeType, mimeType = content.mimeType,
url = content.getFileUrl(), url = content.getFileUrl(),
elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
@ -87,7 +90,8 @@ class RoomEventsAttachmentProvider(
} else if (content is MessageStickerContent) { } else if (content is MessageStickerContent) {
val data = ImageContentRenderer.Data( val data = ImageContentRenderer.Data(
eventId = it.eventId, eventId = it.eventId,
filename = content.body, filename = content.getFileName(),
caption = content.getCaption(),
mimeType = content.mimeType, mimeType = content.mimeType,
url = content.getFileUrl(), url = content.getFileUrl(),
elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
@ -114,7 +118,8 @@ class RoomEventsAttachmentProvider(
} else if (content is MessageVideoContent) { } else if (content is MessageVideoContent) {
val thumbnailData = ImageContentRenderer.Data( val thumbnailData = ImageContentRenderer.Data(
eventId = it.eventId, eventId = it.eventId,
filename = content.body, filename = content.getFileName(),
caption = content.getCaption(),
mimeType = content.mimeType, mimeType = content.mimeType,
url = content.videoInfo?.getThumbnailUrl(), url = content.videoInfo?.getThumbnailUrl(),
elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(), elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(),
@ -126,7 +131,8 @@ class RoomEventsAttachmentProvider(
) )
val data = VideoContentRenderer.Data( val data = VideoContentRenderer.Data(
eventId = it.eventId, eventId = it.eventId,
filename = content.body, filename = content.getFileName(),
caption = content.getCaption(),
mimeType = content.mimeType, mimeType = content.mimeType,
url = content.getFileUrl(), url = content.getFileUrl(),
elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),

View file

@ -49,6 +49,7 @@ class VideoContentRenderer @Inject constructor(
data class Data( data class Data(
override val eventId: String, override val eventId: String,
override val filename: String, override val filename: String,
override val caption: String?,
override val mimeType: String?, override val mimeType: String?,
override val url: String?, override val url: String?,
override val elementToDecrypt: ElementToDecrypt?, override val elementToDecrypt: ElementToDecrypt?,

View file

@ -50,6 +50,8 @@ import im.vector.app.features.roomprofile.uploads.RoomUploadsViewState
import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent 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.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
import javax.inject.Inject import javax.inject.Inject
@ -132,7 +134,8 @@ class RoomUploadsMediaFragment :
is MessageImageContent -> { is MessageImageContent -> {
ImageContentRenderer.Data( ImageContentRenderer.Data(
eventId = it.eventId, eventId = it.eventId,
filename = content.body, filename = content.getFileName(),
caption = content.getCaption(),
mimeType = content.mimeType, mimeType = content.mimeType,
url = content.getFileUrl(), url = content.getFileUrl(),
elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
@ -145,7 +148,8 @@ class RoomUploadsMediaFragment :
is MessageVideoContent -> { is MessageVideoContent -> {
val thumbnailData = ImageContentRenderer.Data( val thumbnailData = ImageContentRenderer.Data(
eventId = it.eventId, eventId = it.eventId,
filename = content.body, filename = content.getFileName(),
caption = content.getCaption(),
mimeType = content.mimeType, mimeType = content.mimeType,
url = content.videoInfo?.getThumbnailUrl(), url = content.videoInfo?.getThumbnailUrl(),
elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(), elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(),
@ -156,7 +160,8 @@ class RoomUploadsMediaFragment :
) )
VideoContentRenderer.Data( VideoContentRenderer.Data(
eventId = it.eventId, eventId = it.eventId,
filename = content.body, filename = content.getFileName(),
caption = content.getCaption(),
mimeType = content.mimeType, mimeType = content.mimeType,
url = content.getFileUrl(), url = content.getFileUrl(),
elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),

View file

@ -30,6 +30,8 @@ import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent 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.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
import org.matrix.android.sdk.api.session.room.uploads.UploadEvent import org.matrix.android.sdk.api.session.room.uploads.UploadEvent
@ -108,7 +110,8 @@ class UploadsMediaController @Inject constructor(
return ImageContentRenderer.Data( return ImageContentRenderer.Data(
eventId = eventId, eventId = eventId,
filename = messageContent.body, filename = messageContent.getFileName(),
caption = messageContent.getCaption(),
url = messageContent.getFileUrl(), url = messageContent.getFileUrl(),
mimeType = messageContent.mimeType, mimeType = messageContent.mimeType,
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
@ -124,7 +127,8 @@ class UploadsMediaController @Inject constructor(
val thumbnailData = ImageContentRenderer.Data( val thumbnailData = ImageContentRenderer.Data(
eventId = eventId, eventId = eventId,
filename = messageContent.body, filename = messageContent.getFileName(),
caption = messageContent.getCaption(),
mimeType = messageContent.mimeType, mimeType = messageContent.mimeType,
url = messageContent.videoInfo?.getThumbnailUrl(), url = messageContent.videoInfo?.getThumbnailUrl(),
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
@ -136,7 +140,8 @@ class UploadsMediaController @Inject constructor(
return VideoContentRenderer.Data( return VideoContentRenderer.Data(
eventId = eventId, eventId = eventId,
filename = messageContent.body, filename = messageContent.getFileName(),
caption = messageContent.getCaption(),
mimeType = messageContent.mimeType, mimeType = messageContent.mimeType,
url = messageContent.getFileUrl(), url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(), elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),

View file

@ -95,6 +95,17 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<im.vector.app.core.ui.views.FooteredTextView
android:id="@+id/messageCaptionView"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="0dp"
android:textColor="?vctr_content_primary"
android:layout_gravity="left"
tools:text="@sample/messages.json/data/message"
tools:ignore="RtlHardcoded" />
<include <include
android:id="@+id/messageFileUploadProgressLayout" android:id="@+id/messageFileUploadProgressLayout"
layout="@layout/media_upload_download_progress_layout" layout="@layout/media_upload_download_progress_layout"

View file

@ -48,6 +48,17 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<im.vector.app.core.ui.views.FooteredTextView
android:id="@+id/messageCaptionView"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="0dp"
android:textColor="?vctr_content_primary"
android:layout_gravity="left"
tools:text="@sample/messages.json/data/message"
tools:ignore="RtlHardcoded" />
<include <include
android:id="@+id/messageFileUploadProgressLayout" android:id="@+id/messageFileUploadProgressLayout"
layout="@layout/media_upload_download_progress_layout" layout="@layout/media_upload_download_progress_layout"

View file

@ -18,6 +18,21 @@
tools:layout_height="300dp" tools:layout_height="300dp"
tools:src="@tools:sample/backgrounds/scenic" /> tools:src="@tools:sample/backgrounds/scenic" />
<im.vector.app.core.ui.views.FooteredTextView
android:id="@+id/messageCaptionView"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="0dp"
android:textColor="?vctr_content_primary"
android:layout_gravity="left"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/messageThumbnailView"
app:layout_constraintHorizontal_bias="0"
tools:text="@sample/messages.json/data/message"
tools:ignore="RtlHardcoded" />
<ImageView <ImageView
android:id="@+id/messageMediaPlayView" android:id="@+id/messageMediaPlayView"
android:layout_width="40dp" android:layout_width="40dp"
@ -42,6 +57,6 @@
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/messageThumbnailView" app:layout_constraintTop_toBottomOf="@id/messageTextView"
tools:visibility="visible" /> tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>