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.BaseFlowNode
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.operation.show
import io.element.android.libraries.di.RoomScope
@@ -324,7 +323,8 @@ class MessagesFlowNode @AssistedInject constructor(
is TimelineItemImageContent -> {
val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo(
name = event.content.filename ?: event.content.body,
filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension
@@ -341,7 +341,8 @@ class MessagesFlowNode @AssistedInject constructor(
if (event.content.preferredMediaSource != null) {
val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo(
name = event.content.body,
filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension
@@ -358,7 +359,8 @@ class MessagesFlowNode @AssistedInject constructor(
is TimelineItemVideoContent -> {
val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo(
name = event.content.filename ?: event.content.body,
filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension
@@ -372,7 +374,8 @@ class MessagesFlowNode @AssistedInject constructor(
is TimelineItemFileContent -> {
val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo(
name = event.content.body,
filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize,
fileExtension = event.content.fileExtension
@@ -386,7 +389,8 @@ class MessagesFlowNode @AssistedInject constructor(
is TimelineItemAudioContent -> {
val navTarget = NavTarget.MediaViewer(
mediaInfo = MediaInfo(
name = event.content.body,
filename = event.content.filename,
caption = event.content.caption,
mimeType = event.content.mimeType,
formattedFileSize = event.content.formattedFileSize,
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)) }
}
is TimelineItemImageContent -> {
content = { ContentForBody(event.content.body) }
content = { ContentForBody(event.content.bestDescription) }
}
is TimelineItemStickerContent -> {
content = { ContentForBody(event.content.body) }
content = { ContentForBody(event.content.bestDescription) }
}
is TimelineItemVideoContent -> {
content = { ContentForBody(event.content.body) }
content = { ContentForBody(event.content.bestDescription) }
}
is TimelineItemFileContent -> {
content = { ContentForBody(event.content.body) }
content = { ContentForBody(event.content.bestDescription) }
}
is TimelineItemAudioContent -> {
content = { ContentForBody(event.content.body) }
content = { ContentForBody(event.content.bestDescription) }
}
is TimelineItemVoiceContent -> {
content = { ContentForBody(textContent) }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -91,7 +91,7 @@ fun TimelineItemImageView(
model = MediaRequestData(
source = content.preferredMediaSource,
kind = MediaRequestData.Kind.File(
body = content.filename ?: content.body,
fileName = content.filename,
mimeType = content.mimeType,
),
),
@@ -108,7 +108,9 @@ fun TimelineItemImageView(
val caption = if (LocalInspectionMode.current) {
SpannedString(content.caption)
} 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(
LocalContentColor provides ElementTheme.colors.textPrimary,
@@ -158,9 +160,9 @@ internal fun TimelineImageWithCaptionRowPreview() = ElementPreview {
ATimelineItemEventRow(
event = aTimelineItemEvent(
isMine = isMine,
content = aTimelineItemImageContent().copy(
content = aTimelineItemImageContent(
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,
),
groupPosition = TimelineItemGroupPosition.Last,
@@ -170,9 +172,9 @@ internal fun TimelineImageWithCaptionRowPreview() = ElementPreview {
ATimelineItemEventRow(
event = aTimelineItemEvent(
isMine = false,
content = aTimelineItemImageContent().copy(
content = aTimelineItemImageContent(
filename = "image.jpg",
body = "Image with null aspectRatio",
caption = "Image with null aspectRatio",
aspectRatio = null,
),
groupPosition = TimelineItemGroupPosition.Last,

View File

@@ -43,7 +43,7 @@ fun TimelineItemStickerView(
onShowClick: () -> Unit,
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(
modifier = modifier.semantics { contentDescription = description },
) {
@@ -65,7 +65,7 @@ fun TimelineItemStickerView(
model = MediaRequestData(
source = content.preferredMediaSource,
kind = MediaRequestData.Kind.File(
body = content.body,
fileName = content.filename,
mimeType = content.mimeType,
),
),

View File

@@ -76,8 +76,8 @@ fun TimelineItemVideoView(
) {
val containerModifier = if (content.showCaption) {
Modifier
.padding(top = 6.dp)
.clip(RoundedCornerShape(6.dp))
.padding(top = 6.dp)
.clip(RoundedCornerShape(6.dp))
} else {
Modifier
}
@@ -93,12 +93,12 @@ fun TimelineItemVideoView(
var isLoaded by remember { mutableStateOf(false) }
AsyncImage(
modifier = Modifier
.fillMaxWidth()
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
.fillMaxWidth()
.then(if (isLoaded) Modifier.background(Color.White) else Modifier),
model = MediaRequestData(
source = content.thumbnailSource,
kind = MediaRequestData.Kind.File(
body = content.filename ?: content.body,
fileName = content.filename,
mimeType = content.mimeType
)
),
@@ -126,7 +126,9 @@ fun TimelineItemVideoView(
val caption = if (LocalInspectionMode.current) {
SpannedString(content.caption)
} 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(
LocalContentColor provides ElementTheme.colors.textPrimary,
@@ -178,7 +180,7 @@ internal fun TimelineVideoWithCaptionRowPreview() = ElementPreview {
isMine = isMine,
content = aTimelineItemVideoContent().copy(
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,
),
groupPosition = TimelineItemGroupPosition.Last,
@@ -190,7 +192,7 @@ internal fun TimelineVideoWithCaptionRowPreview() = ElementPreview {
isMine = false,
content = aTimelineItemVideoContent().copy(
filename = "video.mp4",
body = "Video with null aspect ratio",
caption = "Video with null aspect ratio",
aspectRatio = null,
),
groupPosition = TimelineItemGroupPosition.Last,

View File

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

View File

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

View File

@@ -8,12 +8,23 @@
package io.element.android.features.messages.impl.timeline.model.event
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
@Immutable
sealed interface TimelineItemEventContent {
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.
*/

View File

@@ -8,16 +8,19 @@
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.timeline.item.event.FormattedBody
import io.element.android.libraries.mediaviewer.api.helper.formatFileExtensionAndSize
data class TimelineItemFileContent(
val body: String,
override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val fileSource: MediaSource,
val thumbnailSource: MediaSource?,
val formattedFileSize: String,
val fileExtension: String,
val mimeType: String,
) : TimelineItemEventContent {
) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemFileContent"
val fileExtensionAndSize = formatFileExtensionAndSize(fileExtension, formattedFileSize)

View File

@@ -20,8 +20,12 @@ open class TimelineItemFileContentProvider : PreviewParameterProvider<TimelineIt
)
}
fun aTimelineItemFileContent(fileName: String = "A file.pdf") = TimelineItemFileContent(
body = fileName,
fun aTimelineItemFileContent(
fileName: String = "A file.pdf",
) = TimelineItemFileContent(
filename = fileName,
caption = null,
formattedCaption = null,
thumbnailSource = null,
fileSource = MediaSource(url = ""),
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
data class TimelineItemImageContent(
val body: String,
val formatted: FormattedBody?,
val filename: String?,
override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val mediaSource: MediaSource,
val thumbnailSource: MediaSource?,
val formattedFileSize: String,
@@ -24,11 +24,10 @@ data class TimelineItemImageContent(
val width: Int?,
val height: Int?,
val aspectRatio: Float?
) : TimelineItemEventContent {
) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemImageContent"
val showCaption = filename != null && filename != body
val caption = if (showCaption) body else ""
val showCaption = caption != null
val preferredMediaSource = if (mimeType == MimeTypes.Gif) {
mediaSource

View File

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

View File

@@ -8,9 +8,12 @@
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.timeline.item.event.FormattedBody
data class TimelineItemStickerContent(
val body: String,
override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val mediaSource: MediaSource,
val thumbnailSource: MediaSource?,
val formattedFileSize: String,
@@ -20,7 +23,7 @@ data class TimelineItemStickerContent(
val width: Int?,
val height: Int?,
val aspectRatio: Float?
) : TimelineItemEventContent {
) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemStickerContent"
/* Stickers are supposed to be small images so

View File

@@ -26,7 +26,9 @@ fun aTimelineItemStickerContent(
aspectRatio: Float = 0.5f,
blurhash: String? = A_BLUR_HASH,
) = TimelineItemStickerContent(
body = "a body",
filename = "a sticker.gif",
caption = "a body",
formattedCaption = null,
mediaSource = MediaSource(""),
thumbnailSource = null,
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
data class TimelineItemVideoContent(
val body: String,
val formatted: FormattedBody?,
val filename: String?,
override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val duration: Duration,
val videoSource: MediaSource,
val thumbnailSource: MediaSource?,
@@ -25,9 +25,8 @@ data class TimelineItemVideoContent(
val mimeType: String,
val formattedFileSize: String,
val fileExtension: String,
) : TimelineItemEventContent {
) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemImageContent"
val showCaption = filename != null && filename != body
val caption = if (showCaption) body else ""
val showCaption = caption != null
}

View File

@@ -27,9 +27,9 @@ fun aTimelineItemVideoContent(
aspectRatio: Float = 0.5f,
blurhash: String? = A_BLUR_HASH,
) = TimelineItemVideoContent(
body = "Video.mp4",
formatted = null,
filename = null,
filename = "Video.mp4",
caption = null,
formattedCaption = null,
thumbnailSource = null,
blurHash = blurhash,
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.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody
import kotlinx.collections.immutable.ImmutableList
import kotlin.time.Duration
data class TimelineItemVoiceContent(
val eventId: EventId?,
val body: String,
override val filename: String,
override val caption: String?,
override val formattedCaption: FormattedBody?,
val duration: Duration,
val mediaSource: MediaSource,
val mimeType: String,
val waveform: ImmutableList<Float>,
) : TimelineItemEventContent {
) : TimelineItemEventContentWithAttachment {
override val type: String = "TimelineItemAudioContent"
}

View File

@@ -35,17 +35,21 @@ open class TimelineItemVoiceContentProvider : PreviewParameterProvider<TimelineI
}
fun aTimelineItemVoiceContent(
eventId: String? = "\$anEventId",
body: String = "body doesn't really matter for a voice message",
eventId: EventId? = EventId("\$anEventId"),
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,
contentUri: String = "mxc://matrix.org/1234567890abcdefg",
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),
) = TimelineItemVoiceContent(
eventId = eventId?.let { EventId(it) },
body = body,
eventId = eventId,
filename = filename,
caption = caption,
formattedCaption = null,
duration = duration,
mediaSource = MediaSource(contentUri),
mediaSource = mediaSource,
mimeType = mimeType,
waveform = waveform.toPersistentList(),
)

View File

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

View File

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

View File

@@ -69,7 +69,7 @@ class PinnedMessagesListViewTest {
state = state,
onEventClick = callback
)
rule.onAllNodesWithText(content.body).onFirst().performClick()
rule.onAllNodesWithText(content.filename).onFirst().performClick()
}
}
@@ -85,7 +85,7 @@ class PinnedMessagesListViewTest {
rule.setPinnedMessagesListView(
state = state,
)
rule.onAllNodesWithText(content.body).onFirst()
rule.onAllNodesWithText(content.filename).onFirst()
.performTouchInput {
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.media.aMediaSource
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.mediaviewer.api.util.FileExtensionExtractorWithoutValidation
import kotlinx.collections.immutable.persistentListOf
@@ -228,14 +229,14 @@ class TimelineItemContentMessageFactoryTest {
fun `test create VideoMessageType`() = runTest {
val sut = createTimelineItemContentMessageFactory()
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",
eventId = AN_EVENT_ID,
)
val expected = TimelineItemVideoContent(
body = "body",
formatted = null,
filename = null,
filename = "filename",
caption = null,
formattedCaption = null,
duration = Duration.ZERO,
videoSource = MediaSource(url = "url", json = null),
thumbnailSource = null,
@@ -256,9 +257,9 @@ class TimelineItemContentMessageFactoryTest {
val result = sut.create(
content = createMessageContent(
type = VideoMessageType(
body = "body.mp4 caption",
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
filename = "body.mp4",
caption = "body.mp4 caption",
formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"),
source = MediaSource("url"),
info = VideoInfo(
duration = 1.minutes,
@@ -281,9 +282,9 @@ class TimelineItemContentMessageFactoryTest {
eventId = AN_EVENT_ID,
)
val expected = TimelineItemVideoContent(
body = "body.mp4 caption",
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
filename = "body.mp4",
caption = "body.mp4 caption",
formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"),
duration = 1.minutes,
videoSource = MediaSource(url = "url", json = null),
thumbnailSource = MediaSource("url_thumbnail"),
@@ -302,12 +303,14 @@ class TimelineItemContentMessageFactoryTest {
fun `test create AudioMessageType`() = runTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = AudioMessageType("body", MediaSource("url"), null)),
content = createMessageContent(type = AudioMessageType("filename", null, null, MediaSource("url"), null)),
senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID,
)
val expected = TimelineItemAudioContent(
body = "body",
filename = "filename",
caption = null,
formattedCaption = null,
duration = Duration.ZERO,
mediaSource = MediaSource(url = "url", json = null),
mimeType = MimeTypes.OctetStream,
@@ -323,7 +326,9 @@ class TimelineItemContentMessageFactoryTest {
val result = sut.create(
content = createMessageContent(
type = AudioMessageType(
body = "body.mp3",
filename = "body.mp3",
caption = null,
formattedCaption = null,
source = MediaSource("url"),
info = AudioInfo(
duration = 1.minutes,
@@ -336,7 +341,9 @@ class TimelineItemContentMessageFactoryTest {
eventId = AN_EVENT_ID,
)
val expected = TimelineItemAudioContent(
body = "body.mp3",
filename = "body.mp3",
caption = null,
formattedCaption = null,
duration = 1.minutes,
mediaSource = MediaSource(url = "url", json = null),
mimeType = MimeTypes.Mp3,
@@ -350,13 +357,15 @@ class TimelineItemContentMessageFactoryTest {
fun `test create VoiceMessageType`() = runTest {
val sut = createTimelineItemContentMessageFactory()
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",
eventId = AN_EVENT_ID,
)
val expected = TimelineItemVoiceContent(
filename = "filename",
eventId = AN_EVENT_ID,
body = "body",
caption = null,
formattedCaption = null,
duration = Duration.ZERO,
mediaSource = MediaSource(url = "url", json = null),
mimeType = MimeTypes.OctetStream,
@@ -371,7 +380,9 @@ class TimelineItemContentMessageFactoryTest {
val result = sut.create(
content = createMessageContent(
type = VoiceMessageType(
body = "body.ogg",
filename = "body.ogg",
caption = null,
formattedCaption = null,
source = MediaSource("url"),
info = AudioInfo(
duration = 1.minutes,
@@ -389,7 +400,9 @@ class TimelineItemContentMessageFactoryTest {
)
val expected = TimelineItemVoiceContent(
eventId = AN_EVENT_ID,
body = "body.ogg",
filename = "body.ogg",
caption = null,
formattedCaption = null,
duration = 1.minutes,
mediaSource = MediaSource(url = "url", json = null),
mimeType = MimeTypes.Ogg,
@@ -408,12 +421,14 @@ class TimelineItemContentMessageFactoryTest {
)
)
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",
eventId = AN_EVENT_ID,
)
val expected = TimelineItemAudioContent(
body = "body",
filename = "filename",
caption = null,
formattedCaption = null,
duration = Duration.ZERO,
mediaSource = MediaSource(url = "url", json = null),
mimeType = MimeTypes.OctetStream,
@@ -427,14 +442,14 @@ class TimelineItemContentMessageFactoryTest {
fun `test create ImageMessageType`() = runTest {
val sut = createTimelineItemContentMessageFactory()
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",
eventId = AN_EVENT_ID,
)
val expected = TimelineItemImageContent(
body = "body",
formatted = null,
filename = null,
filename = "filename",
caption = "body",
formattedCaption = null,
mediaSource = MediaSource(url = "url", json = null),
thumbnailSource = null,
formattedFileSize = "0 Bytes",
@@ -453,13 +468,15 @@ class TimelineItemContentMessageFactoryTest {
val sut = createTimelineItemContentStickerFactory()
val result = sut.create(
content = createStickerContent(
"body",
ImageInfo(32, 32, "image/webp", 8192, null, MediaSource("thumbnail://url"), null),
"url"
filename = "filename",
inImageInfo = ImageInfo(32, 32, "image/webp", 8192, null, MediaSource("thumbnail://url"), null),
inUrl = "url"
)
)
val expected = TimelineItemStickerContent(
body = "body",
filename = "filename",
caption = null,
formattedCaption = null,
mediaSource = MediaSource(url = "url", json = null),
thumbnailSource = MediaSource(url = "thumbnail://url", json = null),
formattedFileSize = "8192 Bytes",
@@ -479,9 +496,9 @@ class TimelineItemContentMessageFactoryTest {
val result = sut.create(
content = createMessageContent(
type = ImageMessageType(
body = "body.jpg caption",
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
filename = "body.jpg",
caption = "body.jpg caption",
formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"),
source = MediaSource("url"),
info = ImageInfo(
height = 10L,
@@ -503,9 +520,9 @@ class TimelineItemContentMessageFactoryTest {
eventId = AN_EVENT_ID,
)
val expected = TimelineItemImageContent(
body = "body.jpg caption",
formatted = FormattedBody(MessageFormat.HTML, "formatted"),
filename = "body.jpg",
formattedCaption = FormattedBody(MessageFormat.HTML, "formatted"),
caption = "body.jpg caption",
mediaSource = MediaSource(url = "url", json = null),
thumbnailSource = MediaSource("url_thumbnail"),
formattedFileSize = "888 Bytes",
@@ -523,12 +540,14 @@ class TimelineItemContentMessageFactoryTest {
fun `test create FileMessageType`() = runTest {
val sut = createTimelineItemContentMessageFactory()
val result = sut.create(
content = createMessageContent(type = FileMessageType("body", MediaSource("url"), null)),
content = createMessageContent(type = FileMessageType("filename", null, null, MediaSource("url"), null)),
senderDisambiguatedDisplayName = "Bob",
eventId = AN_EVENT_ID,
)
val expected = TimelineItemFileContent(
body = "body",
filename = "filename",
caption = null,
formattedCaption = null,
fileSource = MediaSource(url = "url", json = null),
thumbnailSource = null,
formattedFileSize = "0 Bytes",
@@ -544,7 +563,9 @@ class TimelineItemContentMessageFactoryTest {
val result = sut.create(
content = createMessageContent(
type = FileMessageType(
body = "body.pdf",
filename = "body.pdf",
caption = null,
formattedCaption = null,
source = MediaSource("url"),
info = FileInfo(
mimetype = MimeTypes.Pdf,
@@ -563,7 +584,9 @@ class TimelineItemContentMessageFactoryTest {
eventId = AN_EVENT_ID,
)
val expected = TimelineItemFileContent(
body = "body.pdf",
filename = "body.pdf",
caption = null,
formattedCaption = null,
fileSource = MediaSource(url = "url", json = null),
thumbnailSource = MediaSource("url_thumbnail"),
formattedFileSize = "123 Bytes",
@@ -749,14 +772,16 @@ class TimelineItemContentMessageFactoryTest {
)
private fun createStickerContent(
body: String = "Body",
filename: String = "filename",
inImageInfo: ImageInfo,
inUrl: String
inUrl: String,
body: String? = null,
): StickerContent {
return StickerContent(
return aStickerContent(
filename = filename,
body = body,
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 input = MediaViewerNode.Inputs(
mediaInfo = MediaInfo(
name = navTarget.name,
filename = navTarget.name,
caption = null,
mimeType = mimeType,
formattedFileSize = "",
fileExtension = ""

View File

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

View File

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

View File

@@ -67,7 +67,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor(
message.prefixIfNeeded(senderDisambiguatedDisplayName, isDmRoom, isOutgoing)
}
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)
}
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.RoomMembershipContent
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.TextMessageType
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.aProfileChangeMessageContent
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.ui.strings.CommonStrings
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
@@ -91,7 +91,7 @@ class DefaultPinnedMessagesBannerFormatterTest {
fun `Sticker content`() {
val body = "a sticker body"
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 expectedBody = "Sticker: a sticker body"
assertThat(result.toString()).isEqualTo(expectedBody)
@@ -135,11 +135,11 @@ class DefaultPinnedMessagesBannerFormatterTest {
val sharedContentMessagesTypes = arrayOf(
TextMessageType(body, null),
VideoMessageType(body, null, null, MediaSource("url"), null),
AudioMessageType(body, MediaSource("url"), null),
VoiceMessageType(body, MediaSource("url"), null, null),
AudioMessageType(body, null, null, MediaSource("url"), null),
VoiceMessageType(body, null, null, MediaSource("url"), null, null),
ImageMessageType(body, null, null, MediaSource("url"), null),
StickerMessageType(body, MediaSource("url"), null),
FileMessageType(body, MediaSource("url"), null),
StickerMessageType(body, null, null, MediaSource("url"), null),
FileMessageType(body, null, null, MediaSource("url"), null),
LocationMessageType(body, "geo:1,2", null),
NoticeMessageType(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.RoomMembershipContent
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.TextMessageType
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.aProfileChangeMessageContent
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.services.toolbox.impl.strings.AndroidStringProvider
import org.junit.Before
@@ -98,7 +98,7 @@ class DefaultRoomLastMessageFormatterTest {
fun `Sticker content`() {
val body = "a sticker body"
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 expectedBody = someoneElseId.toString() + ": Sticker (a sticker body)"
assertThat(result.toString()).isEqualTo(expectedBody)
@@ -179,11 +179,11 @@ class DefaultRoomLastMessageFormatterTest {
val sharedContentMessagesTypes = arrayOf(
TextMessageType(body, null),
VideoMessageType(body, null, null, MediaSource("url"), null),
AudioMessageType(body, MediaSource("url"), null),
VoiceMessageType(body, MediaSource("url"), null, null),
AudioMessageType(body, null, null, MediaSource("url"), null),
VoiceMessageType(body, null, null, MediaSource("url"), null, null),
ImageMessageType(body, null, null, MediaSource("url"), null),
StickerMessageType(body, MediaSource("url"), null),
FileMessageType(body, MediaSource("url"), null),
StickerMessageType(body, null, null, MediaSource("url"), null),
FileMessageType(body, null, null, MediaSource("url"), null),
LocationMessageType(body, "geo:1,2", null),
NoticeMessageType(body, null),
EmoteMessageType(body, null),

View File

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

View File

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

View File

@@ -50,14 +50,18 @@ class EventMessageMapper {
when (type.content.voice) {
null -> {
AudioMessageType(
body = type.content.body,
filename = type.content.filename,
caption = type.content.caption,
formattedCaption = type.content.formattedCaption?.map(),
source = type.content.source.map(),
info = type.content.info?.map(),
)
}
else -> {
VoiceMessageType(
body = type.content.body,
filename = type.content.filename,
caption = type.content.caption,
formattedCaption = type.content.formattedCaption?.map(),
source = type.content.source.map(),
info = type.content.info?.map(),
details = type.content.audio?.map(),
@@ -66,10 +70,22 @@ class EventMessageMapper {
}
}
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 -> {
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 -> {
NoticeMessageType(type.content.body, type.content.formatted?.map())
@@ -81,7 +97,13 @@ class EventMessageMapper {
EmoteMessageType(type.content.body, type.content.formatted?.map())
}
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 -> {
LocationMessageType(type.content.body, type.content.geoUri, type.content.description)

View File

@@ -84,7 +84,8 @@ class TimelineEventContentMapper(
}
is TimelineItemContent.Sticker -> {
StickerContent(
body = it.body,
filename = it.body,
body = null,
info = it.info.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.TransactionId
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.PollKind
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.ProfileTimelineDetails
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.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_USER_ID
@@ -110,6 +113,18 @@ fun aMessageContent(
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(
model: String = "Rust(Model())",
originalJson: String? = null,

View File

@@ -46,7 +46,7 @@ internal class CoilMediaFetcher(
*
*/
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 ->
val file = mediaFile.toFile()
SourceResult(

View File

@@ -26,7 +26,12 @@ data class MediaRequestData(
) {
sealed interface 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 {
constructor(size: Long) : this(size, size)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -157,7 +157,7 @@ class AndroidLocalMediaActions @Inject constructor(
@RequiresApi(Build.VERSION_CODES.Q)
private fun saveOnDiskUsingMediaStore(localMedia: LocalMedia) {
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.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
@@ -175,7 +175,7 @@ class AndroidLocalMediaActions @Inject constructor(
private fun saveOnDiskUsingExternalStorageApi(localMedia: LocalMedia) {
val target = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
localMedia.info.name
localMedia.info.filename
)
localMedia.openStream()?.use { input ->
FileOutputStream(target).use { output ->

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,7 +47,7 @@ interface NotificationMediaRepo {
*
* @param mediaSource the media source 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].
*/
suspend fun getMediaFile(

View File

@@ -187,7 +187,7 @@ class DefaultNotifiableEventResolverTest {
aNotificationData(
content = NotificationContent.MessageLike.RoomMessage(
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(
content = NotificationContent.MessageLike.RoomMessage(
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(
content = NotificationContent.MessageLike.RoomMessage(
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(
content = NotificationContent.MessageLike.RoomMessage(
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(
content = NotificationContent.MessageLike.RoomMessage(
senderId = A_USER_ID_2,
messageType = FileMessageType("File", MediaSource("url"), null),
messageType = FileMessageType("File", null, null, MediaSource("url"), null),
),
)
)