Improve mapping regarding filename, caption and formattedCaption

This commit is contained in:
Benoit Marty 2024-10-01 14:40:53 +02:00
parent e71a010e22
commit f7a3f707fb
56 changed files with 440 additions and 255 deletions

View file

@ -50,7 +50,6 @@ import io.element.android.features.poll.api.create.CreatePollMode
import io.element.android.libraries.architecture.BackstackWithOverlayBox import io.element.android.libraries.architecture.BackstackWithOverlayBox
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.architecture.overlay.Overlay import io.element.android.libraries.architecture.overlay.Overlay
import io.element.android.libraries.architecture.overlay.operation.show import io.element.android.libraries.architecture.overlay.operation.show
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
@ -324,7 +323,8 @@ class MessagesFlowNode @AssistedInject constructor(
is TimelineItemImageContent -> { is TimelineItemImageContent -> {
val navTarget = NavTarget.MediaViewer( val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = event.content.filename ?: event.content.body, filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension
@ -341,7 +341,8 @@ class MessagesFlowNode @AssistedInject constructor(
if (event.content.preferredMediaSource != null) { if (event.content.preferredMediaSource != null) {
val navTarget = NavTarget.MediaViewer( val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = event.content.body, filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension
@ -358,7 +359,8 @@ class MessagesFlowNode @AssistedInject constructor(
is TimelineItemVideoContent -> { is TimelineItemVideoContent -> {
val navTarget = NavTarget.MediaViewer( val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = event.content.filename ?: event.content.body, filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension
@ -372,7 +374,8 @@ class MessagesFlowNode @AssistedInject constructor(
is TimelineItemFileContent -> { is TimelineItemFileContent -> {
val navTarget = NavTarget.MediaViewer( val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = event.content.body, filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension
@ -386,7 +389,8 @@ class MessagesFlowNode @AssistedInject constructor(
is TimelineItemAudioContent -> { is TimelineItemAudioContent -> {
val navTarget = NavTarget.MediaViewer( val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = event.content.body, filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType, mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize, formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension fileExtension = event.content.fileExtension

View file

@ -269,19 +269,19 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
content = { ContentForBody(stringResource(CommonStrings.common_shared_location)) } content = { ContentForBody(stringResource(CommonStrings.common_shared_location)) }
} }
is TimelineItemImageContent -> { is TimelineItemImageContent -> {
content = { ContentForBody(event.content.body) } content = { ContentForBody(event.content.bestDescription) }
} }
is TimelineItemStickerContent -> { is TimelineItemStickerContent -> {
content = { ContentForBody(event.content.body) } content = { ContentForBody(event.content.bestDescription) }
} }
is TimelineItemVideoContent -> { is TimelineItemVideoContent -> {
content = { ContentForBody(event.content.body) } content = { ContentForBody(event.content.bestDescription) }
} }
is TimelineItemFileContent -> { is TimelineItemFileContent -> {
content = { ContentForBody(event.content.body) } content = { ContentForBody(event.content.bestDescription) }
} }
is TimelineItemAudioContent -> { is TimelineItemAudioContent -> {
content = { ContentForBody(event.content.body) } content = { ContentForBody(event.content.bestDescription) }
} }
is TimelineItemVoiceContent -> { is TimelineItemVoiceContent -> {
content = { ContentForBody(textContent) } content = { ContentForBody(textContent) }

View file

@ -629,7 +629,7 @@ internal fun TimelineItemEventRowPreview() = ElementPreview {
ATimelineItemEventRow( ATimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
isMine = isMine, isMine = isMine,
content = aTimelineItemImageContent().copy( content = aTimelineItemImageContent(
aspectRatio = 2.5f aspectRatio = 2.5f
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,

View file

@ -38,7 +38,7 @@ internal fun TimelineItemEventRowForDirectRoomPreview() = ElementPreview {
ATimelineItemEventRow( ATimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
isMine = it, isMine = it,
content = aTimelineItemImageContent().copy( content = aTimelineItemImageContent(
aspectRatio = 5f aspectRatio = 5f
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,

View file

@ -45,7 +45,7 @@ internal fun TimelineItemEventRowShieldPreview() = ElementPreview {
ATimelineItemEventRow( ATimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
isMine = true, isMine = true,
content = aTimelineItemImageContent().copy( content = aTimelineItemImageContent(
aspectRatio = 2.5f aspectRatio = 2.5f
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,
@ -54,7 +54,7 @@ internal fun TimelineItemEventRowShieldPreview() = ElementPreview {
) )
ATimelineItemEventRow( ATimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
content = aTimelineItemImageContent().copy( content = aTimelineItemImageContent(
aspectRatio = 2.5f aspectRatio = 2.5f
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,

View file

@ -49,7 +49,7 @@ internal fun TimelineItemEventRowWithReplyContentToPreview(
event = aTimelineItemEvent( event = aTimelineItemEvent(
isMine = it, isMine = it,
timelineItemReactions = aTimelineItemReactions(count = 0), timelineItemReactions = aTimelineItemReactions(count = 0),
content = aTimelineItemImageContent().copy( content = aTimelineItemImageContent(
aspectRatio = 2.5f aspectRatio = 2.5f
), ),
inReplyTo = inReplyToDetails, inReplyTo = inReplyToDetails,

View file

@ -63,7 +63,7 @@ fun TimelineItemAudioView(
Spacer(Modifier.width(spacing)) Spacer(Modifier.width(spacing))
Column { Column {
Text( Text(
text = content.body, text = content.bestDescription,
color = ElementTheme.materialColors.primary, color = ElementTheme.materialColors.primary,
maxLines = 2, maxLines = 2,
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,

View file

@ -64,7 +64,7 @@ fun TimelineItemFileView(
Spacer(Modifier.width(spacing)) Spacer(Modifier.width(spacing))
Column { Column {
Text( Text(
text = content.body, text = content.bestDescription,
color = ElementTheme.materialColors.primary, color = ElementTheme.materialColors.primary,
maxLines = 2, maxLines = 2,
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,

View file

@ -91,7 +91,7 @@ fun TimelineItemImageView(
model = MediaRequestData( model = MediaRequestData(
source = content.preferredMediaSource, source = content.preferredMediaSource,
kind = MediaRequestData.Kind.File( kind = MediaRequestData.Kind.File(
body = content.filename ?: content.body, fileName = content.filename,
mimeType = content.mimeType, mimeType = content.mimeType,
), ),
), ),
@ -108,7 +108,9 @@ fun TimelineItemImageView(
val caption = if (LocalInspectionMode.current) { val caption = if (LocalInspectionMode.current) {
SpannedString(content.caption) SpannedString(content.caption)
} else { } else {
content.formatted?.body?.takeIf { content.formatted.format == MessageFormat.HTML } ?: SpannedString(content.caption) content.formattedCaption?.body
?.takeIf { content.formattedCaption.format == MessageFormat.HTML }
?: SpannedString(content.caption)
} }
CompositionLocalProvider( CompositionLocalProvider(
LocalContentColor provides ElementTheme.colors.textPrimary, LocalContentColor provides ElementTheme.colors.textPrimary,
@ -158,9 +160,9 @@ internal fun TimelineImageWithCaptionRowPreview() = ElementPreview {
ATimelineItemEventRow( ATimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
isMine = isMine, isMine = isMine,
content = aTimelineItemImageContent().copy( content = aTimelineItemImageContent(
filename = "image.jpg", filename = "image.jpg",
body = "A long caption that may wrap into several lines", caption = "A long caption that may wrap into several lines",
aspectRatio = 2.5f, aspectRatio = 2.5f,
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,
@ -170,9 +172,9 @@ internal fun TimelineImageWithCaptionRowPreview() = ElementPreview {
ATimelineItemEventRow( ATimelineItemEventRow(
event = aTimelineItemEvent( event = aTimelineItemEvent(
isMine = false, isMine = false,
content = aTimelineItemImageContent().copy( content = aTimelineItemImageContent(
filename = "image.jpg", filename = "image.jpg",
body = "Image with null aspectRatio", caption = "Image with null aspectRatio",
aspectRatio = null, aspectRatio = null,
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,

View file

@ -43,7 +43,7 @@ fun TimelineItemStickerView(
onShowClick: () -> Unit, onShowClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val description = content.body.takeIf { it.isNotEmpty() } ?: stringResource(CommonStrings.common_image) val description = content.bestDescription.takeIf { it.isNotEmpty() } ?: stringResource(CommonStrings.common_image)
Column( Column(
modifier = modifier.semantics { contentDescription = description }, modifier = modifier.semantics { contentDescription = description },
) { ) {
@ -65,7 +65,7 @@ fun TimelineItemStickerView(
model = MediaRequestData( model = MediaRequestData(
source = content.preferredMediaSource, source = content.preferredMediaSource,
kind = MediaRequestData.Kind.File( kind = MediaRequestData.Kind.File(
body = content.body, fileName = content.filename,
mimeType = content.mimeType, mimeType = content.mimeType,
), ),
), ),

View file

@ -76,8 +76,8 @@ fun TimelineItemVideoView(
) { ) {
val containerModifier = if (content.showCaption) { val containerModifier = if (content.showCaption) {
Modifier Modifier
.padding(top = 6.dp) .padding(top = 6.dp)
.clip(RoundedCornerShape(6.dp)) .clip(RoundedCornerShape(6.dp))
} else { } else {
Modifier Modifier
} }
@ -93,12 +93,12 @@ fun TimelineItemVideoView(
var isLoaded by remember { mutableStateOf(false) } var isLoaded by remember { mutableStateOf(false) }
AsyncImage( AsyncImage(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.then(if (isLoaded) Modifier.background(Color.White) else Modifier), .then(if (isLoaded) Modifier.background(Color.White) else Modifier),
model = MediaRequestData( model = MediaRequestData(
source = content.thumbnailSource, source = content.thumbnailSource,
kind = MediaRequestData.Kind.File( kind = MediaRequestData.Kind.File(
body = content.filename ?: content.body, fileName = content.filename,
mimeType = content.mimeType mimeType = content.mimeType
) )
), ),
@ -126,7 +126,9 @@ fun TimelineItemVideoView(
val caption = if (LocalInspectionMode.current) { val caption = if (LocalInspectionMode.current) {
SpannedString(content.caption) SpannedString(content.caption)
} else { } else {
content.formatted?.body?.takeIf { content.formatted.format == MessageFormat.HTML } ?: SpannedString(content.caption) content.formattedCaption?.body
?.takeIf { content.formattedCaption.format == MessageFormat.HTML }
?: SpannedString(content.caption)
} }
CompositionLocalProvider( CompositionLocalProvider(
LocalContentColor provides ElementTheme.colors.textPrimary, LocalContentColor provides ElementTheme.colors.textPrimary,
@ -178,7 +180,7 @@ internal fun TimelineVideoWithCaptionRowPreview() = ElementPreview {
isMine = isMine, isMine = isMine,
content = aTimelineItemVideoContent().copy( content = aTimelineItemVideoContent().copy(
filename = "video.mp4", filename = "video.mp4",
body = "A long caption that may wrap into several lines", caption = "A long caption that may wrap into several lines",
aspectRatio = 2.5f, aspectRatio = 2.5f,
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,
@ -190,7 +192,7 @@ internal fun TimelineVideoWithCaptionRowPreview() = ElementPreview {
isMine = false, isMine = false,
content = aTimelineItemVideoContent().copy( content = aTimelineItemVideoContent().copy(
filename = "video.mp4", filename = "video.mp4",
body = "Video with null aspect ratio", caption = "Video with null aspect ratio",
aspectRatio = null, aspectRatio = null,
), ),
groupPosition = TimelineItemGroupPosition.Last, groupPosition = TimelineItemGroupPosition.Last,

View file

@ -84,9 +84,9 @@ class TimelineItemContentMessageFactory @Inject constructor(
is ImageMessageType -> { is ImageMessageType -> {
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
TimelineItemImageContent( TimelineItemImageContent(
body = messageType.body.trimEnd(),
formatted = messageType.formatted,
filename = messageType.filename, filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
mediaSource = messageType.source, mediaSource = messageType.source,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -95,13 +95,15 @@ class TimelineItemContentMessageFactory @Inject constructor(
height = messageType.info?.height?.toInt(), height = messageType.info?.height?.toInt(),
aspectRatio = aspectRatio, aspectRatio = aspectRatio,
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = messageType.filename?.let { fileExtensionExtractor.extractFromName(it) }.orEmpty() fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
) )
} }
is StickerMessageType -> { is StickerMessageType -> {
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
TimelineItemStickerContent( TimelineItemStickerContent(
body = messageType.body.trimEnd(), filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
mediaSource = messageType.source, mediaSource = messageType.source,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -110,7 +112,7 @@ class TimelineItemContentMessageFactory @Inject constructor(
height = messageType.info?.height?.toInt(), height = messageType.info?.height?.toInt(),
aspectRatio = aspectRatio, aspectRatio = aspectRatio,
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = fileExtensionExtractor.extractFromName(messageType.body) fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
) )
} }
is LocationMessageType -> { is LocationMessageType -> {
@ -136,9 +138,9 @@ class TimelineItemContentMessageFactory @Inject constructor(
is VideoMessageType -> { is VideoMessageType -> {
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
TimelineItemVideoContent( TimelineItemVideoContent(
body = messageType.body.trimEnd(),
formatted = messageType.formatted,
filename = messageType.filename, filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
videoSource = messageType.source, videoSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -148,17 +150,19 @@ class TimelineItemContentMessageFactory @Inject constructor(
blurHash = messageType.info?.blurhash, blurHash = messageType.info?.blurhash,
aspectRatio = aspectRatio, aspectRatio = aspectRatio,
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = messageType.filename?.let { fileExtensionExtractor.extractFromName(it) }.orEmpty(), fileExtension = fileExtensionExtractor.extractFromName(messageType.filename),
) )
} }
is AudioMessageType -> { is AudioMessageType -> {
TimelineItemAudioContent( TimelineItemAudioContent(
body = messageType.body.trimEnd(), filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
mediaSource = messageType.source, mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO, duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = fileExtensionExtractor.extractFromName(messageType.body), fileExtension = fileExtensionExtractor.extractFromName(messageType.filename),
) )
} }
is VoiceMessageType -> { is VoiceMessageType -> {
@ -166,7 +170,9 @@ class TimelineItemContentMessageFactory @Inject constructor(
true -> { true -> {
TimelineItemVoiceContent( TimelineItemVoiceContent(
eventId = eventId, eventId = eventId,
body = messageType.body.trimEnd(), filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
mediaSource = messageType.source, mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO, duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
@ -175,20 +181,24 @@ class TimelineItemContentMessageFactory @Inject constructor(
} }
false -> { false -> {
TimelineItemAudioContent( TimelineItemAudioContent(
body = messageType.body.trimEnd(), filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
mediaSource = messageType.source, mediaSource = messageType.source,
duration = messageType.info?.duration ?: Duration.ZERO, duration = messageType.info?.duration ?: Duration.ZERO,
mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream,
formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0),
fileExtension = fileExtensionExtractor.extractFromName(messageType.body), fileExtension = fileExtensionExtractor.extractFromName(messageType.filename),
) )
} }
} }
} }
is FileMessageType -> { is FileMessageType -> {
val fileExtension = fileExtensionExtractor.extractFromName(messageType.body) val fileExtension = fileExtensionExtractor.extractFromName(messageType.filename)
TimelineItemFileContent( TimelineItemFileContent(
body = messageType.body.trimEnd(), filename = messageType.filename,
caption = messageType.caption?.trimEnd(),
formattedCaption = messageType.formattedCaption,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
fileSource = messageType.source, fileSource = messageType.source,
mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension), mimeType = messageType.info?.mimetype ?: MimeTypes.fromFileExtension(fileExtension),

View file

@ -33,7 +33,9 @@ class TimelineItemContentStickerFactory @Inject constructor(
val aspectRatio = aspectRatioOf(content.info.width, content.info.height) val aspectRatio = aspectRatioOf(content.info.width, content.info.height)
return TimelineItemStickerContent( return TimelineItemStickerContent(
body = content.body, filename = content.filename,
caption = content.body,
formattedCaption = null,
mediaSource = content.source, mediaSource = content.source,
thumbnailSource = content.info.thumbnailSource, thumbnailSource = content.info.thumbnailSource,
mimeType = content.info.mimetype ?: MimeTypes.OctetStream, mimeType = content.info.mimetype ?: MimeTypes.OctetStream,
@ -42,7 +44,7 @@ class TimelineItemContentStickerFactory @Inject constructor(
height = content.info.height?.toInt(), height = content.info.height?.toInt(),
aspectRatio = aspectRatio, aspectRatio = aspectRatio,
formattedFileSize = fileSizeFormatter.format(content.info.size ?: 0), formattedFileSize = fileSizeFormatter.format(content.info.size ?: 0),
fileExtension = fileExtensionExtractor.extractFromName(content.body) fileExtension = fileExtensionExtractor.extractFromName(content.filename)
) )
} }
} }

View file

@ -8,17 +8,20 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
import kotlin.time.Duration import kotlin.time.Duration
data class TimelineItemAudioContent( data class TimelineItemAudioContent(
val body: String, override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val duration: Duration, val duration: Duration,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val mimeType: String, val mimeType: String,
val formattedFileSize: String, val formattedFileSize: String,
val fileExtension: String, val fileExtension: String,
) : TimelineItemEventContent { ) : TimelineItemEventContentWithAttachment {
val fileExtensionAndSize = val fileExtensionAndSize =
formatFileExtensionAndSize( formatFileExtensionAndSize(
fileExtension, fileExtension,

View file

@ -22,8 +22,10 @@ open class TimelineItemAudioContentProvider : PreviewParameterProvider<TimelineI
} }
fun aTimelineItemAudioContent(fileName: String = "A sound.mp3") = TimelineItemAudioContent( fun aTimelineItemAudioContent(fileName: String = "A sound.mp3") = TimelineItemAudioContent(
body = fileName, filename = fileName,
mimeType = MimeTypes.Pdf, caption = null,
formattedCaption = null,
mimeType = MimeTypes.Mp3,
formattedFileSize = "100kB", formattedFileSize = "100kB",
fileExtension = "mp3", fileExtension = "mp3",
duration = 100.milliseconds, duration = 100.milliseconds,

View file

@ -8,12 +8,23 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
@Immutable @Immutable
sealed interface TimelineItemEventContent { sealed interface TimelineItemEventContent {
val type: String val type: String
} }
@Immutable
sealed interface TimelineItemEventContentWithAttachment : TimelineItemEventContent {
val filename: String
val caption: String?
val formattedCaption: FormattedBody?
val bestDescription: String
get() = caption ?: filename
}
/** /**
* Only text based content can be copied. * Only text based content can be copied.
*/ */

View file

@ -8,16 +8,19 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
data class TimelineItemFileContent( data class TimelineItemFileContent(
val body: String, override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val fileSource: MediaSource, val fileSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val formattedFileSize: String, val formattedFileSize: String,
val fileExtension: String, val fileExtension: String,
val mimeType: String, val mimeType: String,
) : TimelineItemEventContent { ) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemFileContent" override val type: String = "TimelineItemFileContent"
val fileExtensionAndSize = formatFileExtensionAndSize(fileExtension, formattedFileSize) val fileExtensionAndSize = formatFileExtensionAndSize(fileExtension, formattedFileSize)

View file

@ -20,8 +20,12 @@ open class TimelineItemFileContentProvider : PreviewParameterProvider<TimelineIt
) )
} }
fun aTimelineItemFileContent(fileName: String = "A file.pdf") = TimelineItemFileContent( fun aTimelineItemFileContent(
body = fileName, fileName: String = "A file.pdf",
) = TimelineItemFileContent(
filename = fileName,
caption = null,
formattedCaption = null,
thumbnailSource = null, thumbnailSource = null,
fileSource = MediaSource(url = ""), fileSource = MediaSource(url = ""),
mimeType = MimeTypes.Pdf, mimeType = MimeTypes.Pdf,

View file

@ -12,9 +12,9 @@ import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
data class TimelineItemImageContent( data class TimelineItemImageContent(
val body: String, override val filename: String,
val formatted: FormattedBody?, override val caption: String?,
val filename: String?, override val formattedCaption: FormattedBody?,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val formattedFileSize: String, val formattedFileSize: String,
@ -24,11 +24,10 @@ data class TimelineItemImageContent(
val width: Int?, val width: Int?,
val height: Int?, val height: Int?,
val aspectRatio: Float? val aspectRatio: Float?
) : TimelineItemEventContent { ) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemImageContent" override val type: String = "TimelineItemImageContent"
val showCaption = filename != null && filename != body val showCaption = caption != null
val caption = if (showCaption) body else ""
val preferredMediaSource = if (mimeType == MimeTypes.Gif) { val preferredMediaSource = if (mimeType == MimeTypes.Gif) {
mediaSource mediaSource

View file

@ -23,12 +23,14 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineI
} }
fun aTimelineItemImageContent( fun aTimelineItemImageContent(
aspectRatio: Float = 0.5f, aspectRatio: Float? = 0.5f,
blurhash: String? = A_BLUR_HASH, blurhash: String? = A_BLUR_HASH,
filename: String = "A picture.jpg",
caption: String? = null,
) = TimelineItemImageContent( ) = TimelineItemImageContent(
body = "a body", filename = filename,
formatted = null, caption = caption,
filename = null, formattedCaption = null,
mediaSource = MediaSource(""), mediaSource = MediaSource(""),
thumbnailSource = null, thumbnailSource = null,
mimeType = MimeTypes.IMAGE_JPEG, mimeType = MimeTypes.IMAGE_JPEG,

View file

@ -8,9 +8,12 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
data class TimelineItemStickerContent( data class TimelineItemStickerContent(
val body: String, override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
val formattedFileSize: String, val formattedFileSize: String,
@ -20,7 +23,7 @@ data class TimelineItemStickerContent(
val width: Int?, val width: Int?,
val height: Int?, val height: Int?,
val aspectRatio: Float? val aspectRatio: Float?
) : TimelineItemEventContent { ) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemStickerContent" override val type: String = "TimelineItemStickerContent"
/* Stickers are supposed to be small images so /* Stickers are supposed to be small images so

View file

@ -26,7 +26,9 @@ fun aTimelineItemStickerContent(
aspectRatio: Float = 0.5f, aspectRatio: Float = 0.5f,
blurhash: String? = A_BLUR_HASH, blurhash: String? = A_BLUR_HASH,
) = TimelineItemStickerContent( ) = TimelineItemStickerContent(
body = "a body", filename = "a sticker.gif",
caption = "a body",
formattedCaption = null,
mediaSource = MediaSource(""), mediaSource = MediaSource(""),
thumbnailSource = null, thumbnailSource = null,
mimeType = MimeTypes.IMAGE_JPEG, mimeType = MimeTypes.IMAGE_JPEG,

View file

@ -12,9 +12,9 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import kotlin.time.Duration import kotlin.time.Duration
data class TimelineItemVideoContent( data class TimelineItemVideoContent(
val body: String, override val filename: String,
val formatted: FormattedBody?, override val caption: String?,
val filename: String?, override val formattedCaption: FormattedBody?,
val duration: Duration, val duration: Duration,
val videoSource: MediaSource, val videoSource: MediaSource,
val thumbnailSource: MediaSource?, val thumbnailSource: MediaSource?,
@ -25,9 +25,8 @@ data class TimelineItemVideoContent(
val mimeType: String, val mimeType: String,
val formattedFileSize: String, val formattedFileSize: String,
val fileExtension: String, val fileExtension: String,
) : TimelineItemEventContent { ) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemImageContent" override val type: String = "TimelineItemImageContent"
val showCaption = filename != null && filename != body val showCaption = caption != null
val caption = if (showCaption) body else ""
} }

View file

@ -27,9 +27,9 @@ fun aTimelineItemVideoContent(
aspectRatio: Float = 0.5f, aspectRatio: Float = 0.5f,
blurhash: String? = A_BLUR_HASH, blurhash: String? = A_BLUR_HASH,
) = TimelineItemVideoContent( ) = TimelineItemVideoContent(
body = "Video.mp4", filename = "Video.mp4",
formatted = null, caption = null,
filename = null, formattedCaption = null,
thumbnailSource = null, thumbnailSource = null,
blurHash = blurhash, blurHash = blurhash,
aspectRatio = aspectRatio, aspectRatio = aspectRatio,

View file

@ -9,16 +9,19 @@ package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlin.time.Duration import kotlin.time.Duration
data class TimelineItemVoiceContent( data class TimelineItemVoiceContent(
val eventId: EventId?, val eventId: EventId?,
val body: String, override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val duration: Duration, val duration: Duration,
val mediaSource: MediaSource, val mediaSource: MediaSource,
val mimeType: String, val mimeType: String,
val waveform: ImmutableList<Float>, val waveform: ImmutableList<Float>,
) : TimelineItemEventContent { ) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemAudioContent" override val type: String = "TimelineItemAudioContent"
} }

View file

@ -35,17 +35,21 @@ open class TimelineItemVoiceContentProvider : PreviewParameterProvider<TimelineI
} }
fun aTimelineItemVoiceContent( fun aTimelineItemVoiceContent(
eventId: String? = "\$anEventId", eventId: EventId? = EventId("\$anEventId"),
body: String = "body doesn't really matter for a voice message", filename: String = "filename doesn't really matter for a voice message",
caption: String? = "body doesn't really matter for a voice message",
duration: Duration = 61_000.milliseconds, duration: Duration = 61_000.milliseconds,
contentUri: String = "mxc://matrix.org/1234567890abcdefg", contentUri: String = "mxc://matrix.org/1234567890abcdefg",
mimeType: String = MimeTypes.Ogg, mimeType: String = MimeTypes.Ogg,
mediaSource: MediaSource = MediaSource(contentUri),
waveform: List<Float> = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f), waveform: List<Float> = listOf(0f, 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f, 8f, 7f, 6f, 5f, 4f, 3f, 2f, 1f, 0f),
) = TimelineItemVoiceContent( ) = TimelineItemVoiceContent(
eventId = eventId?.let { EventId(it) }, eventId = eventId,
body = body, filename = filename,
caption = caption,
formattedCaption = null,
duration = duration, duration = duration,
mediaSource = MediaSource(contentUri), mediaSource = mediaSource,
mimeType = mimeType, mimeType = mimeType,
waveform = waveform.toPersistentList(), waveform = waveform.toPersistentList(),
) )

View file

@ -59,7 +59,7 @@ class VoiceMessagePresenter @AssistedInject constructor(
eventId = content.eventId, eventId = content.eventId,
mediaSource = content.mediaSource, mediaSource = content.mediaSource,
mimeType = content.mimeType, mimeType = content.mimeType,
body = content.body, body = content.caption,
) )
private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized) private val play = mutableStateOf<AsyncData<Unit>>(AsyncData.Uninitialized)

View file

@ -334,9 +334,9 @@ class MessagesPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
val mediaMessage = aMessageEvent( val mediaMessage = aMessageEvent(
content = TimelineItemImageContent( content = TimelineItemImageContent(
body = "image.jpg", filename = "image.jpg",
formatted = null, caption = null,
filename = null, formattedCaption = null,
mediaSource = MediaSource(AN_AVATAR_URL), mediaSource = MediaSource(AN_AVATAR_URL),
thumbnailSource = null, thumbnailSource = null,
mimeType = MimeTypes.Jpeg, mimeType = MimeTypes.Jpeg,
@ -373,9 +373,9 @@ class MessagesPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
val mediaMessage = aMessageEvent( val mediaMessage = aMessageEvent(
content = TimelineItemVideoContent( content = TimelineItemVideoContent(
body = "video.mp4", filename = "video.mp4",
formatted = null, caption = null,
filename = null, formattedCaption = null,
duration = 10.milliseconds, duration = 10.milliseconds,
videoSource = MediaSource(AN_AVATAR_URL), videoSource = MediaSource(AN_AVATAR_URL),
thumbnailSource = MediaSource(AN_AVATAR_URL), thumbnailSource = MediaSource(AN_AVATAR_URL),
@ -413,7 +413,9 @@ class MessagesPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
val mediaMessage = aMessageEvent( val mediaMessage = aMessageEvent(
content = TimelineItemFileContent( content = TimelineItemFileContent(
body = "file.pdf", filename = "file.pdf",
caption = null,
formattedCaption = null,
fileSource = MediaSource(AN_AVATAR_URL), fileSource = MediaSource(AN_AVATAR_URL),
thumbnailSource = MediaSource(AN_AVATAR_URL), thumbnailSource = MediaSource(AN_AVATAR_URL),
formattedFileSize = "10 MB", formattedFileSize = "10 MB",

View file

@ -69,7 +69,7 @@ class PinnedMessagesListViewTest {
state = state, state = state,
onEventClick = callback onEventClick = callback
) )
rule.onAllNodesWithText(content.body).onFirst().performClick() rule.onAllNodesWithText(content.filename).onFirst().performClick()
} }
} }
@ -85,7 +85,7 @@ class PinnedMessagesListViewTest {
rule.setPinnedMessagesListView( rule.setPinnedMessagesListView(
state = state, state = state,
) )
rule.onAllNodesWithText(content.body).onFirst() rule.onAllNodesWithText(content.filename).onFirst()
.performTouchInput { .performTouchInput {
longClick() longClick()
} }

View file

@ -62,6 +62,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageT
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.media.aMediaSource import io.element.android.libraries.matrix.test.media.aMediaSource
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.timeline.aStickerContent
import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH import io.element.android.libraries.matrix.ui.components.A_BLUR_HASH
import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -228,14 +229,14 @@ class TimelineItemContentMessageFactoryTest {
fun `test create VideoMessageType`() = runTest { fun `test create VideoMessageType`() = runTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = VideoMessageType("body", null, null, MediaSource("url"), null)), content = createMessageContent(type = VideoMessageType("filename", null, null, MediaSource("url"), null)),
senderDisambiguatedDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemVideoContent( val expected = TimelineItemVideoContent(
body = "body", filename = "filename",
formatted = null, caption = null,
filename = null, formattedCaption = null,
duration = Duration.ZERO, duration = Duration.ZERO,
videoSource = MediaSource(url = "url", json = null), videoSource = MediaSource(url = "url", json = null),
thumbnailSource = null, thumbnailSource = null,
@ -256,9 +257,9 @@ class TimelineItemContentMessageFactoryTest {
val result = sut.create( val result = sut.create(
content = createMessageContent( content = createMessageContent(
type = VideoMessageType( type = VideoMessageType(
body = "body.mp4 caption",
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
filename = "body.mp4", filename = "body.mp4",
caption = "body.mp4 caption",
formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"),
source = MediaSource("url"), source = MediaSource("url"),
info = VideoInfo( info = VideoInfo(
duration = 1.minutes, duration = 1.minutes,
@ -281,9 +282,9 @@ class TimelineItemContentMessageFactoryTest {
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemVideoContent( val expected = TimelineItemVideoContent(
body = "body.mp4 caption",
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
filename = "body.mp4", filename = "body.mp4",
caption = "body.mp4 caption",
formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"),
duration = 1.minutes, duration = 1.minutes,
videoSource = MediaSource(url = "url", json = null), videoSource = MediaSource(url = "url", json = null),
thumbnailSource = MediaSource("url_thumbnail"), thumbnailSource = MediaSource("url_thumbnail"),
@ -302,12 +303,14 @@ class TimelineItemContentMessageFactoryTest {
fun `test create AudioMessageType`() = runTest { fun `test create AudioMessageType`() = runTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = AudioMessageType("body", MediaSource("url"), null)), content = createMessageContent(type = AudioMessageType("filename", null, null, MediaSource("url"), null)),
senderDisambiguatedDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemAudioContent( val expected = TimelineItemAudioContent(
body = "body", filename = "filename",
caption = null,
formattedCaption = null,
duration = Duration.ZERO, duration = Duration.ZERO,
mediaSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
mimeType = MimeTypes.OctetStream, mimeType = MimeTypes.OctetStream,
@ -323,7 +326,9 @@ class TimelineItemContentMessageFactoryTest {
val result = sut.create( val result = sut.create(
content = createMessageContent( content = createMessageContent(
type = AudioMessageType( type = AudioMessageType(
body = "body.mp3", filename = "body.mp3",
caption = null,
formattedCaption = null,
source = MediaSource("url"), source = MediaSource("url"),
info = AudioInfo( info = AudioInfo(
duration = 1.minutes, duration = 1.minutes,
@ -336,7 +341,9 @@ class TimelineItemContentMessageFactoryTest {
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemAudioContent( val expected = TimelineItemAudioContent(
body = "body.mp3", filename = "body.mp3",
caption = null,
formattedCaption = null,
duration = 1.minutes, duration = 1.minutes,
mediaSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
mimeType = MimeTypes.Mp3, mimeType = MimeTypes.Mp3,
@ -350,13 +357,15 @@ class TimelineItemContentMessageFactoryTest {
fun `test create VoiceMessageType`() = runTest { fun `test create VoiceMessageType`() = runTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = VoiceMessageType("body", MediaSource("url"), null, null)), content = createMessageContent(type = VoiceMessageType("filename", null, null, MediaSource("url"), null, null)),
senderDisambiguatedDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemVoiceContent( val expected = TimelineItemVoiceContent(
filename = "filename",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
body = "body", caption = null,
formattedCaption = null,
duration = Duration.ZERO, duration = Duration.ZERO,
mediaSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
mimeType = MimeTypes.OctetStream, mimeType = MimeTypes.OctetStream,
@ -371,7 +380,9 @@ class TimelineItemContentMessageFactoryTest {
val result = sut.create( val result = sut.create(
content = createMessageContent( content = createMessageContent(
type = VoiceMessageType( type = VoiceMessageType(
body = "body.ogg", filename = "body.ogg",
caption = null,
formattedCaption = null,
source = MediaSource("url"), source = MediaSource("url"),
info = AudioInfo( info = AudioInfo(
duration = 1.minutes, duration = 1.minutes,
@ -389,7 +400,9 @@ class TimelineItemContentMessageFactoryTest {
) )
val expected = TimelineItemVoiceContent( val expected = TimelineItemVoiceContent(
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
body = "body.ogg", filename = "body.ogg",
caption = null,
formattedCaption = null,
duration = 1.minutes, duration = 1.minutes,
mediaSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
mimeType = MimeTypes.Ogg, mimeType = MimeTypes.Ogg,
@ -408,12 +421,14 @@ class TimelineItemContentMessageFactoryTest {
) )
) )
val result = sut.create( val result = sut.create(
content = createMessageContent(type = VoiceMessageType("body", MediaSource("url"), null, null)), content = createMessageContent(type = VoiceMessageType("filename", null, null, MediaSource("url"), null, null)),
senderDisambiguatedDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemAudioContent( val expected = TimelineItemAudioContent(
body = "body", filename = "filename",
caption = null,
formattedCaption = null,
duration = Duration.ZERO, duration = Duration.ZERO,
mediaSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
mimeType = MimeTypes.OctetStream, mimeType = MimeTypes.OctetStream,
@ -427,14 +442,14 @@ class TimelineItemContentMessageFactoryTest {
fun `test create ImageMessageType`() = runTest { fun `test create ImageMessageType`() = runTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = ImageMessageType("body", null, null, MediaSource("url"), null)), content = createMessageContent(type = ImageMessageType("filename", "body", null, MediaSource("url"), null)),
senderDisambiguatedDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemImageContent( val expected = TimelineItemImageContent(
body = "body", filename = "filename",
formatted = null, caption = "body",
filename = null, formattedCaption = null,
mediaSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
thumbnailSource = null, thumbnailSource = null,
formattedFileSize = "0 Bytes", formattedFileSize = "0 Bytes",
@ -453,13 +468,15 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentStickerFactory() val sut = createTimelineItemContentStickerFactory()
val result = sut.create( val result = sut.create(
content = createStickerContent( content = createStickerContent(
"body", filename = "filename",
ImageInfo(32, 32, "image/webp", 8192, null, MediaSource("thumbnail://url"), null), inImageInfo = ImageInfo(32, 32, "image/webp", 8192, null, MediaSource("thumbnail://url"), null),
"url" inUrl = "url"
) )
) )
val expected = TimelineItemStickerContent( val expected = TimelineItemStickerContent(
body = "body", filename = "filename",
caption = null,
formattedCaption = null,
mediaSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
thumbnailSource = MediaSource(url = "thumbnail://url", json = null), thumbnailSource = MediaSource(url = "thumbnail://url", json = null),
formattedFileSize = "8192 Bytes", formattedFileSize = "8192 Bytes",
@ -479,9 +496,9 @@ class TimelineItemContentMessageFactoryTest {
val result = sut.create( val result = sut.create(
content = createMessageContent( content = createMessageContent(
type = ImageMessageType( type = ImageMessageType(
body = "body.jpg caption",
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
filename = "body.jpg", filename = "body.jpg",
caption = "body.jpg caption",
formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"),
source = MediaSource("url"), source = MediaSource("url"),
info = ImageInfo( info = ImageInfo(
height = 10L, height = 10L,
@ -503,9 +520,9 @@ class TimelineItemContentMessageFactoryTest {
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemImageContent( val expected = TimelineItemImageContent(
body = "body.jpg caption",
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
filename = "body.jpg", filename = "body.jpg",
formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"),
caption = "body.jpg caption",
mediaSource = MediaSource(url = "url", json = null), mediaSource = MediaSource(url = "url", json = null),
thumbnailSource = MediaSource("url_thumbnail"), thumbnailSource = MediaSource("url_thumbnail"),
formattedFileSize = "888 Bytes", formattedFileSize = "888 Bytes",
@ -523,12 +540,14 @@ class TimelineItemContentMessageFactoryTest {
fun `test create FileMessageType`() = runTest { fun `test create FileMessageType`() = runTest {
val sut = createTimelineItemContentMessageFactory() val sut = createTimelineItemContentMessageFactory()
val result = sut.create( val result = sut.create(
content = createMessageContent(type = FileMessageType("body", MediaSource("url"), null)), content = createMessageContent(type = FileMessageType("filename", null, null, MediaSource("url"), null)),
senderDisambiguatedDisplayName = "Bob", senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemFileContent( val expected = TimelineItemFileContent(
body = "body", filename = "filename",
caption = null,
formattedCaption = null,
fileSource = MediaSource(url = "url", json = null), fileSource = MediaSource(url = "url", json = null),
thumbnailSource = null, thumbnailSource = null,
formattedFileSize = "0 Bytes", formattedFileSize = "0 Bytes",
@ -544,7 +563,9 @@ class TimelineItemContentMessageFactoryTest {
val result = sut.create( val result = sut.create(
content = createMessageContent( content = createMessageContent(
type = FileMessageType( type = FileMessageType(
body = "body.pdf", filename = "body.pdf",
caption = null,
formattedCaption = null,
source = MediaSource("url"), source = MediaSource("url"),
info = FileInfo( info = FileInfo(
mimetype = MimeTypes.Pdf, mimetype = MimeTypes.Pdf,
@ -563,7 +584,9 @@ class TimelineItemContentMessageFactoryTest {
eventId = AN_EVENT_ID, eventId = AN_EVENT_ID,
) )
val expected = TimelineItemFileContent( val expected = TimelineItemFileContent(
body = "body.pdf", filename = "body.pdf",
caption = null,
formattedCaption = null,
fileSource = MediaSource(url = "url", json = null), fileSource = MediaSource(url = "url", json = null),
thumbnailSource = MediaSource("url_thumbnail"), thumbnailSource = MediaSource("url_thumbnail"),
formattedFileSize = "123 Bytes", formattedFileSize = "123 Bytes",
@ -749,14 +772,16 @@ class TimelineItemContentMessageFactoryTest {
) )
private fun createStickerContent( private fun createStickerContent(
body: String = "Body", filename: String = "filename",
inImageInfo: ImageInfo, inImageInfo: ImageInfo,
inUrl: String inUrl: String,
body: String? = null,
): StickerContent { ): StickerContent {
return StickerContent( return aStickerContent(
filename = filename,
body = body, body = body,
info = inImageInfo, info = inImageInfo,
source = aMediaSource(url = inUrl), mediaSource = aMediaSource(url = inUrl),
) )
} }

View file

@ -206,7 +206,8 @@ class RoomDetailsFlowNode @AssistedInject constructor(
val mimeType = MimeTypes.Images val mimeType = MimeTypes.Images
val input = MediaViewerNode.Inputs( val input = MediaViewerNode.Inputs(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = navTarget.name, filename = navTarget.name,
caption = null,
mimeType = mimeType, mimeType = mimeType,
formattedFileSize = "", formattedFileSize = "",
fileExtension = "" fileExtension = ""

View file

@ -84,10 +84,11 @@ class UserProfileFlowNode @AssistedInject constructor(
val mimeType = MimeTypes.Images val mimeType = MimeTypes.Images
val input = MediaViewerNode.Inputs( val input = MediaViewerNode.Inputs(
mediaInfo = MediaInfo( mediaInfo = MediaInfo(
name = navTarget.name, filename = navTarget.name,
caption = null,
mimeType = mimeType, mimeType = mimeType,
formattedFileSize = "", formattedFileSize = "",
fileExtension = "" fileExtension = "",
), ),
mediaSource = MediaSource(url = navTarget.avatarUrl), mediaSource = MediaSource(url = navTarget.avatarUrl),
thumbnailSource = null, thumbnailSource = null,

View file

@ -46,7 +46,8 @@ class DefaultPinnedMessagesBannerFormatter @Inject constructor(
return when (val content = event.content) { return when (val content = event.content) {
is MessageContent -> processMessageContents(event, content) is MessageContent -> processMessageContents(event, content)
is StickerContent -> { is StickerContent -> {
content.body.prefixWith(CommonStrings.common_sticker) val text = content.body ?: content.filename
text.prefixWith(CommonStrings.common_sticker)
} }
is UnableToDecryptContent -> { is UnableToDecryptContent -> {
sp.getString(CommonStrings.common_waiting_for_decryption_key) sp.getString(CommonStrings.common_waiting_for_decryption_key)
@ -76,25 +77,25 @@ class DefaultPinnedMessagesBannerFormatter @Inject constructor(
messageType.toPlainText(permalinkParser) messageType.toPlainText(permalinkParser)
} }
is VideoMessageType -> { is VideoMessageType -> {
messageType.body.prefixWith(CommonStrings.common_video) messageType.bestDescription.prefixWith(CommonStrings.common_video)
} }
is ImageMessageType -> { is ImageMessageType -> {
messageType.body.prefixWith(CommonStrings.common_image) messageType.bestDescription.prefixWith(CommonStrings.common_image)
} }
is StickerMessageType -> { is StickerMessageType -> {
messageType.body.prefixWith(CommonStrings.common_sticker) messageType.bestDescription.prefixWith(CommonStrings.common_sticker)
} }
is LocationMessageType -> { is LocationMessageType -> {
messageType.body.prefixWith(CommonStrings.common_shared_location) messageType.body.prefixWith(CommonStrings.common_shared_location)
} }
is FileMessageType -> { is FileMessageType -> {
messageType.body.prefixWith(CommonStrings.common_file) messageType.bestDescription.prefixWith(CommonStrings.common_file)
} }
is AudioMessageType -> { is AudioMessageType -> {
messageType.body.prefixWith(CommonStrings.common_audio) messageType.bestDescription.prefixWith(CommonStrings.common_audio)
} }
is VoiceMessageType -> { is VoiceMessageType -> {
messageType.body.prefixWith(CommonStrings.common_voice_message) messageType.bestDescription.prefixWith(CommonStrings.common_voice_message)
} }
is OtherMessageType -> { is OtherMessageType -> {
messageType.body messageType.body

View file

@ -67,7 +67,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
} }
is StickerContent -> { is StickerContent -> {
val message = sp.getString(CommonStrings.common_sticker) + " (" + content.body + ")" val message = sp.getString(CommonStrings.common_sticker) + " (" + content.bestDescription + ")"
message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing) message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
} }
is UnableToDecryptContent -> { is UnableToDecryptContent -> {

View file

@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
@ -46,6 +45,7 @@ import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.timeline.aPollContent import io.element.android.libraries.matrix.test.timeline.aPollContent
import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent
import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails
import io.element.android.libraries.matrix.test.timeline.aStickerContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
@ -91,7 +91,7 @@ class DefaultPinnedMessagesBannerFormatterTest {
fun `Sticker content`() { fun `Sticker content`() {
val body = "a sticker body" val body = "a sticker body"
val info = ImageInfo(null, null, null, null, null, null, null) val info = ImageInfo(null, null, null, null, null, null, null)
val message = createRoomEvent(false, null, StickerContent(body, info, aMediaSource(url = "url"))) val message = createRoomEvent(false, null, aStickerContent(body, info, aMediaSource(url = "url")))
val result = formatter.format(message) val result = formatter.format(message)
val expectedBody = "Sticker: a sticker body" val expectedBody = "Sticker: a sticker body"
assertThat(result.toString()).isEqualTo(expectedBody) assertThat(result.toString()).isEqualTo(expectedBody)
@ -135,11 +135,11 @@ class DefaultPinnedMessagesBannerFormatterTest {
val sharedContentMessagesTypes = arrayOf( val sharedContentMessagesTypes = arrayOf(
TextMessageType(body, null), TextMessageType(body, null),
VideoMessageType(body, null, null, MediaSource("url"), null), VideoMessageType(body, null, null, MediaSource("url"), null),
AudioMessageType(body, MediaSource("url"), null), AudioMessageType(body, null, null, MediaSource("url"), null),
VoiceMessageType(body, MediaSource("url"), null, null), VoiceMessageType(body, null, null, MediaSource("url"), null, null),
ImageMessageType(body, null, null, MediaSource("url"), null), ImageMessageType(body, null, null, MediaSource("url"), null),
StickerMessageType(body, MediaSource("url"), null), StickerMessageType(body, null, null, MediaSource("url"), null),
FileMessageType(body, MediaSource("url"), null), FileMessageType(body, null, null, MediaSource("url"), null),
LocationMessageType(body, "geo:1,2", null), LocationMessageType(body, "geo:1,2", null),
NoticeMessageType(body, null), NoticeMessageType(body, null),
EmoteMessageType(body, null), EmoteMessageType(body, null),

View file

@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.OtherState
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
@ -46,6 +45,7 @@ import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.matrix.test.timeline.aPollContent import io.element.android.libraries.matrix.test.timeline.aPollContent
import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent import io.element.android.libraries.matrix.test.timeline.aProfileChangeMessageContent
import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails import io.element.android.libraries.matrix.test.timeline.aProfileTimelineDetails
import io.element.android.libraries.matrix.test.timeline.aStickerContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
import org.junit.Before import org.junit.Before
@ -98,7 +98,7 @@ class DefaultRoomLastMessageFormatterTest {
fun `Sticker content`() { fun `Sticker content`() {
val body = "a sticker body" val body = "a sticker body"
val info = ImageInfo(null, null, null, null, null, null, null) val info = ImageInfo(null, null, null, null, null, null, null)
val message = createRoomEvent(false, null, StickerContent(body, info, aMediaSource(url = "url"))) val message = createRoomEvent(false, null, aStickerContent(body, info, aMediaSource(url = "url")))
val result = formatter.format(message, false) val result = formatter.format(message, false)
val expectedBody = someoneElseId.toString() + ": Sticker (a sticker body)" val expectedBody = someoneElseId.toString() + ": Sticker (a sticker body)"
assertThat(result.toString()).isEqualTo(expectedBody) assertThat(result.toString()).isEqualTo(expectedBody)
@ -179,11 +179,11 @@ class DefaultRoomLastMessageFormatterTest {
val sharedContentMessagesTypes = arrayOf( val sharedContentMessagesTypes = arrayOf(
TextMessageType(body, null), TextMessageType(body, null),
VideoMessageType(body, null, null, MediaSource("url"), null), VideoMessageType(body, null, null, MediaSource("url"), null),
AudioMessageType(body, MediaSource("url"), null), AudioMessageType(body, null, null, MediaSource("url"), null),
VoiceMessageType(body, MediaSource("url"), null, null), VoiceMessageType(body, null, null, MediaSource("url"), null, null),
ImageMessageType(body, null, null, MediaSource("url"), null), ImageMessageType(body, null, null, MediaSource("url"), null),
StickerMessageType(body, MediaSource("url"), null), StickerMessageType(body, null, null, MediaSource("url"), null),
FileMessageType(body, MediaSource("url"), null), FileMessageType(body, null, null, MediaSource("url"), null),
LocationMessageType(body, "geo:1,2", null), LocationMessageType(body, "geo:1,2", null),
NoticeMessageType(body, null), NoticeMessageType(body, null),
EmoteMessageType(body, null), EmoteMessageType(body, null),

View file

@ -30,10 +30,14 @@ data class MessageContent(
data object RedactedContent : EventContent data object RedactedContent : EventContent
data class StickerContent( data class StickerContent(
val body: String, val filename: String,
val body: String?,
val info: ImageInfo, val info: ImageInfo,
val source: MediaSource, val source: MediaSource,
) : EventContent ) : EventContent {
val bestDescription: String
get() = body ?: filename
}
data class PollContent( data class PollContent(
val question: String, val question: String,

View file

@ -18,24 +18,37 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
@Immutable @Immutable
sealed interface MessageType sealed interface MessageType
@Immutable
sealed interface MessageTypeWithAttachment : MessageType {
val filename: String
val caption: String?
val formattedCaption: FormattedBody?
val bestDescription: String
get() = caption ?: filename
}
data class EmoteMessageType( data class EmoteMessageType(
val body: String, val body: String,
val formatted: FormattedBody? val formatted: FormattedBody?
) : MessageType ) : MessageType
data class ImageMessageType( data class ImageMessageType(
val body: String, override val filename: String,
val formatted: FormattedBody?, override val caption: String?,
val filename: String?, override val formattedCaption: FormattedBody?,
val source: MediaSource, val source: MediaSource,
val info: ImageInfo? val info: ImageInfo?
) : MessageType ) : MessageTypeWithAttachment
// FIXME This is never used in production code.
data class StickerMessageType( data class StickerMessageType(
val body: String, override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val source: MediaSource, val source: MediaSource,
val info: ImageInfo? val info: ImageInfo?
) : MessageType ) : MessageTypeWithAttachment
data class LocationMessageType( data class LocationMessageType(
val body: String, val body: String,
@ -44,31 +57,37 @@ data class LocationMessageType(
) : MessageType ) : MessageType
data class AudioMessageType( data class AudioMessageType(
val body: String, override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val source: MediaSource, val source: MediaSource,
val info: AudioInfo?, val info: AudioInfo?,
) : MessageType ) : MessageTypeWithAttachment
data class VoiceMessageType( data class VoiceMessageType(
val body: String, override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val source: MediaSource, val source: MediaSource,
val info: AudioInfo?, val info: AudioInfo?,
val details: AudioDetails?, val details: AudioDetails?,
) : MessageType ) : MessageTypeWithAttachment
data class VideoMessageType( data class VideoMessageType(
val body: String, override val filename: String,
val formatted: FormattedBody?, override val caption: String?,
val filename: String?, override val formattedCaption: FormattedBody?,
val source: MediaSource, val source: MediaSource,
val info: VideoInfo? val info: VideoInfo?
) : MessageType ) : MessageTypeWithAttachment
data class FileMessageType( data class FileMessageType(
val body: String, override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val source: MediaSource, val source: MediaSource,
val info: FileInfo? val info: FileInfo?
) : MessageType ) : MessageTypeWithAttachment
data class NoticeMessageType( data class NoticeMessageType(
val body: String, val body: String,

View file

@ -50,14 +50,18 @@ class EventMessageMapper {
when (type.content.voice) { when (type.content.voice) {
null -> { null -> {
AudioMessageType( AudioMessageType(
body = type.content.body, filename = type.content.filename,
caption = type.content.caption,
formattedCaption = type.content.formattedCaption?.map(),
source = type.content.source.map(), source = type.content.source.map(),
info = type.content.info?.map(), info = type.content.info?.map(),
) )
} }
else -> { else -> {
VoiceMessageType( VoiceMessageType(
body = type.content.body, filename = type.content.filename,
caption = type.content.caption,
formattedCaption = type.content.formattedCaption?.map(),
source = type.content.source.map(), source = type.content.source.map(),
info = type.content.info?.map(), info = type.content.info?.map(),
details = type.content.audio?.map(), details = type.content.audio?.map(),
@ -66,10 +70,22 @@ class EventMessageMapper {
} }
} }
is RustMessageType.File -> { is RustMessageType.File -> {
FileMessageType(type.content.body, type.content.source.map(), type.content.info?.map()) FileMessageType(
filename = type.content.filename,
caption = type.content.caption,
formattedCaption = type.content.formattedCaption?.map(),
source = type.content.source.map(),
info = type.content.info?.map(),
)
} }
is RustMessageType.Image -> { is RustMessageType.Image -> {
ImageMessageType(type.content.body, type.content.formatted?.map(), type.content.filename, type.content.source.map(), type.content.info?.map()) ImageMessageType(
filename = type.content.filename,
caption = type.content.caption,
formattedCaption = type.content.formattedCaption?.map(),
source = type.content.source.map(),
info = type.content.info?.map(),
)
} }
is RustMessageType.Notice -> { is RustMessageType.Notice -> {
NoticeMessageType(type.content.body, type.content.formatted?.map()) NoticeMessageType(type.content.body, type.content.formatted?.map())
@ -81,7 +97,13 @@ class EventMessageMapper {
EmoteMessageType(type.content.body, type.content.formatted?.map()) EmoteMessageType(type.content.body, type.content.formatted?.map())
} }
is RustMessageType.Video -> { is RustMessageType.Video -> {
VideoMessageType(type.content.body, type.content.formatted?.map(), type.content.filename, type.content.source.map(), type.content.info?.map()) VideoMessageType(
filename = type.content.filename,
caption = type.content.caption,
formattedCaption = type.content.formattedCaption?.map(),
source = type.content.source.map(),
info = type.content.info?.map(),
)
} }
is RustMessageType.Location -> { is RustMessageType.Location -> {
LocationMessageType(type.content.body, type.content.geoUri, type.content.description) LocationMessageType(type.content.body, type.content.geoUri, type.content.description)

View file

@ -84,7 +84,8 @@ class TimelineEventContentMapper(
} }
is TimelineItemContent.Sticker -> { is TimelineItemContent.Sticker -> {
StickerContent( StickerContent(
body = it.body, filename = it.body,
body = null,
info = it.info.map(), info = it.info.map(),
source = it.source.map(), source = it.source.map(),
) )

View file

@ -10,6 +10,8 @@ package io.element.android.libraries.matrix.test.timeline
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollAnswer
import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
@ -25,6 +27,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent
import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
@ -110,6 +113,18 @@ fun aMessageContent(
type = messageType type = messageType
) )
fun aStickerContent(
filename: String = "filename",
info: ImageInfo,
mediaSource: MediaSource,
body: String? = null,
) = StickerContent(
filename = filename,
body = body,
info = info,
source = mediaSource,
)
fun aTimelineItemDebugInfo( fun aTimelineItemDebugInfo(
model: String = "Rust(Model())", model: String = "Rust(Model())",
originalJson: String? = null, originalJson: String? = null,

View file

@ -46,7 +46,7 @@ internal class CoilMediaFetcher(
* *
*/ */
private suspend fun fetchFile(mediaSource: MediaSource, kind: MediaRequestData.Kind.File): FetchResult? { private suspend fun fetchFile(mediaSource: MediaSource, kind: MediaRequestData.Kind.File): FetchResult? {
return mediaLoader.downloadMediaFile(mediaSource, kind.mimeType, kind.body) return mediaLoader.downloadMediaFile(mediaSource, kind.mimeType, kind.fileName)
.map { mediaFile -> .map { mediaFile ->
val file = mediaFile.toFile() val file = mediaFile.toFile()
SourceResult( SourceResult(

View file

@ -26,7 +26,12 @@ data class MediaRequestData(
) { ) {
sealed interface Kind { sealed interface Kind {
data object Content : Kind data object Content : Kind
data class File(val body: String?, val mimeType: String) : Kind
data class File(
val fileName: String,
val mimeType: String,
) : Kind
data class Thumbnail(val width: Long, val height: Long) : Kind { data class Thumbnail(val width: Long, val height: Long) : Kind {
constructor(size: Long) : this(size, size) constructor(size: Long) : this(size, size)
} }

View file

@ -49,11 +49,11 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
), ),
aMessageContent( aMessageContent(
body = "Audio", body = "Audio",
type = AudioMessageType("Audio", MediaSource("url"), null), type = AudioMessageType("Audio", null, null, MediaSource("url"), null),
), ),
aMessageContent( aMessageContent(
body = "Voice", body = "Voice",
type = VoiceMessageType("Voice", MediaSource("url"), null, null), type = VoiceMessageType("Voice", null, null, MediaSource("url"), null, null),
), ),
aMessageContent( aMessageContent(
body = "Image", body = "Image",
@ -61,11 +61,11 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails>
), ),
aMessageContent( aMessageContent(
body = "Sticker", body = "Sticker",
type = StickerMessageType("Image", MediaSource("url"), null), type = StickerMessageType("Image", null, null, MediaSource("url"), null),
), ),
aMessageContent( aMessageContent(
body = "File", body = "File",
type = FileMessageType("File", MediaSource("url"), null), type = FileMessageType("File", null, null, MediaSource("url"), null),
), ),
aMessageContent( aMessageContent(
body = "Location", body = "Location",

View file

@ -75,9 +75,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady( anInReplyToDetailsReady(
eventContent = aMessageContent( eventContent = aMessageContent(
messageType = ImageMessageType( messageType = ImageMessageType(
body = "body", filename = "filename",
formatted = null, caption = null,
filename = null, formattedCaption = null,
source = aMediaSource(), source = aMediaSource(),
info = anImageInfo(), info = anImageInfo(),
) )
@ -105,9 +105,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady( anInReplyToDetailsReady(
eventContent = aMessageContent( eventContent = aMessageContent(
messageType = ImageMessageType( messageType = ImageMessageType(
body = "body", filename = "filename",
formatted = null, caption = "caption",
filename = null, formattedCaption = null,
source = aMediaSource(), source = aMediaSource(),
info = anImageInfo(), info = anImageInfo(),
) )
@ -134,6 +134,7 @@ class InReplyToMetadataKtTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
anInReplyToDetailsReady( anInReplyToDetailsReady(
eventContent = StickerContent( eventContent = StickerContent(
filename = "filename",
body = "body", body = "body",
info = anImageInfo(), info = anImageInfo(),
source = aMediaSource(url = "url") source = aMediaSource(url = "url")
@ -160,6 +161,7 @@ class InReplyToMetadataKtTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
anInReplyToDetailsReady( anInReplyToDetailsReady(
eventContent = StickerContent( eventContent = StickerContent(
filename = "filename",
body = "body", body = "body",
info = anImageInfo(), info = anImageInfo(),
source = aMediaSource(url = "url") source = aMediaSource(url = "url")
@ -187,9 +189,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady( anInReplyToDetailsReady(
eventContent = aMessageContent( eventContent = aMessageContent(
messageType = VideoMessageType( messageType = VideoMessageType(
body = "body", filename = "filename",
formatted = null, caption = null,
filename = null, formattedCaption = null,
source = aMediaSource(), source = aMediaSource(),
info = aVideoInfo(), info = aVideoInfo(),
) )
@ -217,9 +219,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady( anInReplyToDetailsReady(
eventContent = aMessageContent( eventContent = aMessageContent(
messageType = VideoMessageType( messageType = VideoMessageType(
body = "body", filename = "filename",
formatted = null, caption = "caption",
filename = null, formattedCaption = null,
source = aMediaSource(), source = aMediaSource(),
info = aVideoInfo(), info = aVideoInfo(),
) )
@ -247,7 +249,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady( anInReplyToDetailsReady(
eventContent = aMessageContent( eventContent = aMessageContent(
messageType = FileMessageType( messageType = FileMessageType(
body = "body", filename = "filename",
caption = "caption",
formattedCaption = null,
source = aMediaSource(), source = aMediaSource(),
info = FileInfo( info = FileInfo(
mimetype = null, mimetype = null,
@ -280,7 +284,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady( anInReplyToDetailsReady(
eventContent = aMessageContent( eventContent = aMessageContent(
messageType = FileMessageType( messageType = FileMessageType(
body = "body", filename = "filename",
caption = "caption",
formattedCaption = null,
source = aMediaSource(), source = aMediaSource(),
info = FileInfo( info = FileInfo(
mimetype = null, mimetype = null,
@ -313,7 +319,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady( anInReplyToDetailsReady(
eventContent = aMessageContent( eventContent = aMessageContent(
messageType = AudioMessageType( messageType = AudioMessageType(
body = "body", filename = "filename",
caption = "caption",
formattedCaption = null,
source = aMediaSource(), source = aMediaSource(),
info = AudioInfo( info = AudioInfo(
duration = null, duration = null,
@ -375,7 +383,9 @@ class InReplyToMetadataKtTest {
anInReplyToDetailsReady( anInReplyToDetailsReady(
eventContent = aMessageContent( eventContent = aMessageContent(
messageType = VoiceMessageType( messageType = VoiceMessageType(
body = "body", filename = "filename",
caption = "caption",
formattedCaption = null,
source = aMediaSource(), source = aMediaSource(),
info = null, info = null,
details = null, details = null,

View file

@ -303,7 +303,7 @@ private fun MediaFileView(
if (info != null) { if (info != null) {
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
Text( Text(
text = info.name, text = info.filename,
maxLines = 2, maxLines = 2,
style = ElementTheme.typography.fontBodyLgRegular, style = ElementTheme.typography.fontBodyLgRegular,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,

View file

@ -13,43 +13,49 @@ import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class MediaInfo( data class MediaInfo(
val name: String, val filename: String,
val caption: String?,
val mimeType: String, val mimeType: String,
val formattedFileSize: String, val formattedFileSize: String,
val fileExtension: String, val fileExtension: String,
) : Parcelable ) : Parcelable
fun anImageMediaInfo(): MediaInfo = MediaInfo( fun anImageMediaInfo(): MediaInfo = MediaInfo(
"an image file.jpg", filename = "an image file.jpg",
MimeTypes.Jpeg, caption = null,
"4MB", mimeType = MimeTypes.Jpeg,
"jpg" formattedFileSize = "4MB",
fileExtension = "jpg",
) )
fun aVideoMediaInfo(): MediaInfo = MediaInfo( fun aVideoMediaInfo(): MediaInfo = MediaInfo(
"a video file.mp4", filename = "a video file.mp4",
MimeTypes.Mp4, caption = null,
"14MB", mimeType = MimeTypes.Mp4,
"mp4" formattedFileSize = "14MB",
fileExtension = "mp4",
) )
fun aPdfMediaInfo(): MediaInfo = MediaInfo( fun aPdfMediaInfo(): MediaInfo = MediaInfo(
"a pdf file.pdf", filename = "a pdf file.pdf",
MimeTypes.Pdf, caption = null,
"23MB", mimeType = MimeTypes.Pdf,
"pdf" formattedFileSize = "23MB",
fileExtension = "pdf",
) )
fun anApkMediaInfo(): MediaInfo = MediaInfo( fun anApkMediaInfo(): MediaInfo = MediaInfo(
"an apk file.apk", filename = "an apk file.apk",
MimeTypes.Apk, caption = null,
"50MB", mimeType = MimeTypes.Apk,
"apk" formattedFileSize = "50MB",
fileExtension = "apk",
) )
fun anAudioMediaInfo(): MediaInfo = MediaInfo( fun anAudioMediaInfo(): MediaInfo = MediaInfo(
"an audio file.mp3", filename = "an audio file.mp3",
MimeTypes.Mp3, caption = null,
"7MB", mimeType = MimeTypes.Mp3,
"mp3" formattedFileSize = "7MB",
fileExtension = "mp3",
) )

View file

@ -92,7 +92,7 @@ class MediaViewerPresenter @AssistedInject constructor(
mediaLoader.downloadMediaFile( mediaLoader.downloadMediaFile(
source = inputs.mediaSource, source = inputs.mediaSource,
mimeType = inputs.mediaInfo.mimeType, mimeType = inputs.mediaInfo.mimeType,
body = inputs.mediaInfo.name body = inputs.mediaInfo.filename
) )
.onSuccess { .onSuccess {
mediaFile.value = it mediaFile.value = it

View file

@ -322,7 +322,7 @@ private fun ThumbnailView(
if (isVisible) { if (isVisible) {
val mediaRequestData = MediaRequestData( val mediaRequestData = MediaRequestData(
source = thumbnailSource, source = thumbnailSource,
kind = MediaRequestData.Kind.File(mediaInfo.name, mediaInfo.mimeType) kind = MediaRequestData.Kind.File(mediaInfo.filename, mediaInfo.mimeType)
) )
AsyncImage( AsyncImage(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),

View file

@ -157,7 +157,7 @@ class AndroidLocalMediaActions @Inject constructor(
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
private fun saveOnDiskUsingMediaStore(localMedia: LocalMedia) { private fun saveOnDiskUsingMediaStore(localMedia: LocalMedia) {
val contentValues = ContentValues().apply { val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, localMedia.info.name) put(MediaStore.MediaColumns.DISPLAY_NAME, localMedia.info.filename)
put(MediaStore.MediaColumns.MIME_TYPE, localMedia.info.mimeType) put(MediaStore.MediaColumns.MIME_TYPE, localMedia.info.mimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
} }
@ -175,7 +175,7 @@ class AndroidLocalMediaActions @Inject constructor(
private fun saveOnDiskUsingExternalStorageApi(localMedia: LocalMedia) { private fun saveOnDiskUsingExternalStorageApi(localMedia: LocalMedia) {
val target = File( val target = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
localMedia.info.name localMedia.info.filename
) )
localMedia.openStream()?.use { input -> localMedia.openStream()?.use { input ->
FileOutputStream(target).use { output -> FileOutputStream(target).use { output ->

View file

@ -32,21 +32,36 @@ class AndroidLocalMediaFactory @Inject constructor(
private val fileSizeFormatter: FileSizeFormatter, private val fileSizeFormatter: FileSizeFormatter,
private val fileExtensionExtractor: FileExtensionExtractor, private val fileExtensionExtractor: FileExtensionExtractor,
) : LocalMediaFactory { ) : LocalMediaFactory {
override fun createFromMediaFile(mediaFile: MediaFile, mediaInfo: MediaInfo): LocalMedia { override fun createFromMediaFile(
val uri = mediaFile.toFile().toUri() mediaFile: MediaFile,
return createFromUri( mediaInfo: MediaInfo,
uri = uri, ): LocalMedia = createFromUri(
mimeType = mediaInfo.mimeType, uri = mediaFile.toFile().toUri(),
name = mediaInfo.name, mimeType = mediaInfo.mimeType,
formattedFileSize = mediaInfo.formattedFileSize, name = mediaInfo.filename,
) caption = mediaInfo.caption,
} formattedFileSize = mediaInfo.formattedFileSize,
)
override fun createFromUri( override fun createFromUri(
uri: Uri, uri: Uri,
mimeType: String?, mimeType: String?,
name: String?, name: String?,
formattedFileSize: String? formattedFileSize: String?
): LocalMedia = createFromUri(
uri = uri,
mimeType = mimeType,
name = name,
caption = null,
formattedFileSize = formattedFileSize,
)
private fun createFromUri(
uri: Uri,
mimeType: String?,
name: String?,
caption: String?,
formattedFileSize: String?
): LocalMedia { ): LocalMedia {
val resolvedMimeType = mimeType ?: context.getMimeType(uri) ?: MimeTypes.OctetStream val resolvedMimeType = mimeType ?: context.getMimeType(uri) ?: MimeTypes.OctetStream
val fileName = name ?: context.getFileName(uri) ?: "" val fileName = name ?: context.getFileName(uri) ?: ""
@ -56,7 +71,8 @@ class AndroidLocalMediaFactory @Inject constructor(
uri = uri, uri = uri,
info = MediaInfo( info = MediaInfo(
mimeType = resolvedMimeType, mimeType = resolvedMimeType,
name = fileName, filename = fileName,
caption = caption,
formattedFileSize = fileSize, formattedFileSize = fileSize,
fileExtension = fileExtension fileExtension = fileExtension
) )

View file

@ -29,7 +29,8 @@ class AndroidLocalMediaFactoryTest {
assertThat(result.uri.toString()).endsWith("aPath") assertThat(result.uri.toString()).endsWith("aPath")
assertThat(result.info).isEqualTo( assertThat(result.info).isEqualTo(
MediaInfo( MediaInfo(
name = "an image file.jpg", filename = "an image file.jpg",
caption = null,
mimeType = MimeTypes.Jpeg, mimeType = MimeTypes.Jpeg,
formattedFileSize = "4MB", formattedFileSize = "4MB",
fileExtension = "jpg", fileExtension = "jpg",

View file

@ -32,7 +32,8 @@ class FakeLocalMediaFactory(
override fun createFromUri(uri: Uri, mimeType: String?, name: String?, formattedFileSize: String?): LocalMedia { override fun createFromUri(uri: Uri, mimeType: String?, name: String?, formattedFileSize: String?): LocalMedia {
val safeName = name ?: fallbackName val safeName = name ?: fallbackName
val mediaInfo = MediaInfo( val mediaInfo = MediaInfo(
name = safeName, filename = safeName,
caption = null,
mimeType = mimeType ?: fallbackMimeType, mimeType = mimeType ?: fallbackMimeType,
formattedFileSize = formattedFileSize ?: fallbackFileSize, formattedFileSize = formattedFileSize ?: fallbackFileSize,
fileExtension = fileExtensionExtractor.extractFromName(safeName) fileExtension = fileExtensionExtractor.extractFromName(safeName)

View file

@ -265,15 +265,15 @@ class DefaultNotifiableEventResolver @Inject constructor(
senderDisambiguatedDisplayName: String, senderDisambiguatedDisplayName: String,
): String { ): String {
return when (val messageType = content.messageType) { return when (val messageType = content.messageType) {
is AudioMessageType -> messageType.body is AudioMessageType -> messageType.bestDescription
is VoiceMessageType -> stringProvider.getString(CommonStrings.common_voice_message) is VoiceMessageType -> stringProvider.getString(CommonStrings.common_voice_message)
is EmoteMessageType -> "* $senderDisambiguatedDisplayName ${messageType.body}" is EmoteMessageType -> "* $senderDisambiguatedDisplayName ${messageType.body}"
is FileMessageType -> messageType.body is FileMessageType -> messageType.bestDescription
is ImageMessageType -> messageType.body is ImageMessageType -> messageType.bestDescription
is StickerMessageType -> messageType.body is StickerMessageType -> messageType.bestDescription
is NoticeMessageType -> messageType.body is NoticeMessageType -> messageType.body
is TextMessageType -> messageType.toPlainText(permalinkParser = permalinkParser) is TextMessageType -> messageType.toPlainText(permalinkParser = permalinkParser)
is VideoMessageType -> messageType.body is VideoMessageType -> messageType.bestDescription
is LocationMessageType -> messageType.body is LocationMessageType -> messageType.body
is OtherMessageType -> messageType.body is OtherMessageType -> messageType.body
} }
@ -299,7 +299,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
.getMediaFile( .getMediaFile(
mediaSource = messageType.source, mediaSource = messageType.source,
mimeType = messageType.info?.mimetype, mimeType = messageType.info?.mimetype,
body = messageType.body, body = messageType.filename,
) )
is VideoMessageType -> null // Use the thumbnail here? is VideoMessageType -> null // Use the thumbnail here?
else -> null else -> null

View file

@ -47,7 +47,7 @@ interface NotificationMediaRepo {
* *
* @param mediaSource the media source of the media. * @param mediaSource the media source of the media.
* @param mimeType the mime type of the media. * @param mimeType the mime type of the media.
* @param body the body of the message. * @param body optional body which will be used to name the file.
* @return A [Result] holding either the media [File] from the cache directory or an [Exception]. * @return A [Result] holding either the media [File] from the cache directory or an [Exception].
*/ */
suspend fun getMediaFile( suspend fun getMediaFile(

View file

@ -187,7 +187,7 @@ class DefaultNotifiableEventResolverTest {
aNotificationData( aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = AudioMessageType(body = "Audio", MediaSource("url"), null) messageType = AudioMessageType("Audio", null, null, MediaSource("url"), null)
), ),
) )
) )
@ -206,7 +206,7 @@ class DefaultNotifiableEventResolverTest {
aNotificationData( aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = VideoMessageType(body = "Video", null, null, MediaSource("url"), null) messageType = VideoMessageType("Video", null, null, MediaSource("url"), null)
), ),
) )
) )
@ -225,7 +225,7 @@ class DefaultNotifiableEventResolverTest {
aNotificationData( aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = VoiceMessageType(body = "Voice", MediaSource("url"), null, null) messageType = VoiceMessageType("Voice", null, null, MediaSource("url"), null, null)
), ),
) )
) )
@ -263,7 +263,7 @@ class DefaultNotifiableEventResolverTest {
aNotificationData( aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = StickerMessageType("Sticker", MediaSource("url"), null), messageType = StickerMessageType("Sticker", null, null, MediaSource("url"), null),
), ),
) )
) )
@ -282,7 +282,7 @@ class DefaultNotifiableEventResolverTest {
aNotificationData( aNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2, senderId = A_USER_ID_2,
messageType = FileMessageType("File", MediaSource("url"), null), messageType = FileMessageType("File", null, null, MediaSource("url"), null),
), ),
) )
) )