Browse Source

Merge pull request #3574 from element-hq/feature/bma/improveMediaModel

Clarify model for Event with attachment
pull/3693/head
Benoit Marty 2 days ago committed by GitHub
parent
commit
7ece687740
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 16
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt
  2. 10
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt
  3. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt
  4. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowForDirectRoomPreview.kt
  5. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowShieldPreview.kt
  6. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt
  7. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt
  8. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt
  9. 14
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt
  10. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt
  11. 18
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt
  12. 40
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt
  13. 6
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentStickerFactory.kt
  14. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt
  15. 6
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt
  16. 11
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt
  17. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt
  18. 8
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentProvider.kt
  19. 11
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt
  20. 10
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt
  21. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContent.kt
  22. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContentProvider.kt
  23. 11
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt
  24. 6
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt
  25. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt
  26. 14
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt
  27. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt
  28. 16
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt
  29. 4
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt
  30. 101
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt
  31. 3
      features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt
  32. 5
      features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt
  33. 15
      libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt
  34. 2
      libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt
  35. 12
      libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt
  36. 12
      libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt
  37. 8
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt
  38. 51
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt
  39. 32
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt
  40. 3
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt
  41. 15
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt
  42. 2
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt
  43. 7
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt
  44. 8
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt
  45. 42
      libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt
  46. 2
      libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaView.kt
  47. 48
      libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/MediaInfo.kt
  48. 2
      libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerPresenter.kt
  49. 2
      libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerView.kt
  50. 4
      libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt
  51. 36
      libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt
  52. 3
      libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt
  53. 3
      libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt
  54. 12
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt
  55. 2
      libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationMediaRepo.kt
  56. 10
      libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt
  57. 4
      tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_SheetContent_Day_3_en.png
  58. 4
      tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_SheetContent_Night_3_en.png

16
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt

@ -50,7 +50,6 @@ import io.element.android.features.poll.api.create.CreatePollMode @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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

10
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt

@ -269,19 +269,19 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif @@ -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) }

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt

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

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowForDirectRoomPreview.kt

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

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowShieldPreview.kt

@ -45,7 +45,7 @@ internal fun TimelineItemEventRowShieldPreview() = ElementPreview { @@ -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 { @@ -54,7 +54,7 @@ internal fun TimelineItemEventRowShieldPreview() = ElementPreview {
)
ATimelineItemEventRow(
event = aTimelineItemEvent(
content = aTimelineItemImageContent().copy(
content = aTimelineItemImageContent(
aspectRatio = 2.5f
),
groupPosition = TimelineItemGroupPosition.Last,

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRowWithReplyPreview.kt

@ -49,7 +49,7 @@ internal fun TimelineItemEventRowWithReplyContentToPreview( @@ -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,

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt

@ -63,7 +63,7 @@ fun TimelineItemAudioView( @@ -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,

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt

@ -64,7 +64,7 @@ fun TimelineItemFileView( @@ -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,

14
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt

@ -91,7 +91,7 @@ fun TimelineItemImageView( @@ -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( @@ -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 { @@ -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 { @@ -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,

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemStickerView.kt

@ -43,7 +43,7 @@ fun TimelineItemStickerView( @@ -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( @@ -65,7 +65,7 @@ fun TimelineItemStickerView(
model = MediaRequestData(
source = content.preferredMediaSource,
kind = MediaRequestData.Kind.File(
body = content.body,
fileName = content.filename,
mimeType = content.mimeType,
),
),

18
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt

@ -76,8 +76,8 @@ fun TimelineItemVideoView( @@ -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( @@ -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( @@ -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 { @@ -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 { @@ -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,

40
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt

@ -84,9 +84,9 @@ class TimelineItemContentMessageFactory @Inject constructor( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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),

6
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentStickerFactory.kt

@ -33,7 +33,9 @@ class TimelineItemContentStickerFactory @Inject constructor( @@ -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( @@ -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)
)
}
}

7
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt

@ -8,17 +8,20 @@ @@ -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,

6
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt

@ -22,8 +22,10 @@ open class TimelineItemAudioContentProvider : PreviewParameterProvider<TimelineI @@ -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,

11
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContent.kt

@ -8,12 +8,23 @@ @@ -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.
*/

7
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt

@ -8,16 +8,19 @@ @@ -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)

8
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentProvider.kt

@ -20,8 +20,12 @@ open class TimelineItemFileContentProvider : PreviewParameterProvider<TimelineIt @@ -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,

11
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt

@ -12,9 +12,9 @@ import io.element.android.libraries.matrix.api.media.MediaSource @@ -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( @@ -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

10
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt

@ -23,12 +23,14 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineI @@ -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,

7
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContent.kt

@ -8,9 +8,12 @@ @@ -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( @@ -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

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemStickerContentProvider.kt

@ -26,7 +26,9 @@ fun aTimelineItemStickerContent( @@ -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,

11
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt

@ -12,9 +12,9 @@ import io.element.android.libraries.matrix.api.timeline.item.event.FormattedBody @@ -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( @@ -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
}

6
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt

@ -27,9 +27,9 @@ fun aTimelineItemVideoContent( @@ -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,

7
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContent.kt

@ -9,16 +9,19 @@ package io.element.android.features.messages.impl.timeline.model.event @@ -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"
}

14
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVoiceContentProvider.kt

@ -35,17 +35,21 @@ open class TimelineItemVoiceContentProvider : PreviewParameterProvider<TimelineI @@ -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(),
)

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/voicemessages/timeline/VoiceMessagePresenter.kt

@ -59,7 +59,7 @@ class VoiceMessagePresenter @AssistedInject constructor( @@ -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)

16
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt

@ -335,9 +335,9 @@ class MessagesPresenterTest { @@ -335,9 +335,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,
@ -374,9 +374,9 @@ class MessagesPresenterTest { @@ -374,9 +374,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),
@ -414,7 +414,9 @@ class MessagesPresenterTest { @@ -414,7 +414,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",

4
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListViewTest.kt

@ -69,7 +69,7 @@ class PinnedMessagesListViewTest { @@ -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 { @@ -85,7 +85,7 @@ class PinnedMessagesListViewTest {
rule.setPinnedMessagesListView(
state = state,
)
rule.onAllNodesWithText(content.body).onFirst()
rule.onAllNodesWithText(content.filename).onFirst()
.performTouchInput {
longClick()
}

101
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactoryTest.kt

@ -62,6 +62,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageT @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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),
)
}

3
features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt

@ -206,7 +206,8 @@ class RoomDetailsFlowNode @AssistedInject constructor( @@ -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 = ""

5
features/userprofile/impl/src/main/kotlin/io/element/android/features/userprofile/impl/UserProfileFlowNode.kt

@ -84,10 +84,11 @@ class UserProfileFlowNode @AssistedInject constructor( @@ -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,

15
libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt

@ -46,7 +46,8 @@ class DefaultPinnedMessagesBannerFormatter @Inject constructor( @@ -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( @@ -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

2
libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt

@ -67,7 +67,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor( @@ -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 -> {

12
libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatterTest.kt

@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.OtherState @@ -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 @@ -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 { @@ -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 { @@ -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),

12
libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt

@ -32,7 +32,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.OtherState @@ -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 @@ -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 { @@ -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 { @@ -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),

8
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt

@ -30,10 +30,14 @@ data class MessageContent( @@ -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,

51
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt

@ -18,24 +18,37 @@ import io.element.android.libraries.matrix.api.media.VideoInfo @@ -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( @@ -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,

32
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt

@ -50,14 +50,18 @@ class EventMessageMapper { @@ -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 { @@ -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 { @@ -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)

3
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt

@ -84,7 +84,8 @@ class TimelineEventContentMapper( @@ -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(),
)

15
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt

@ -10,6 +10,8 @@ package io.element.android.libraries.matrix.test.timeline @@ -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 @@ -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( @@ -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,

2
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt

@ -44,7 +44,7 @@ internal class CoilMediaFetcher( @@ -44,7 +44,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(

7
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt

@ -26,7 +26,12 @@ data class MediaRequestData( @@ -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)
}

8
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToDetailsProvider.kt

@ -49,11 +49,11 @@ open class InReplyToDetailsProvider : PreviewParameterProvider<InReplyToDetails> @@ -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> @@ -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",

42
libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt

@ -75,9 +75,9 @@ class InReplyToMetadataKtTest { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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,

2
libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/LocalMediaView.kt

@ -303,7 +303,7 @@ private fun MediaFileView( @@ -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,

48
libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/local/MediaInfo.kt

@ -13,43 +13,49 @@ import kotlinx.parcelize.Parcelize @@ -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",
)

2
libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerPresenter.kt

@ -92,7 +92,7 @@ class MediaViewerPresenter @AssistedInject constructor( @@ -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

2
libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/viewer/MediaViewerView.kt

@ -322,7 +322,7 @@ private fun ThumbnailView( @@ -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(),

4
libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaActions.kt

@ -157,7 +157,7 @@ class AndroidLocalMediaActions @Inject constructor( @@ -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( @@ -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 ->

36
libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt

@ -32,21 +32,36 @@ class AndroidLocalMediaFactory @Inject constructor( @@ -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( @@ -56,7 +71,8 @@ class AndroidLocalMediaFactory @Inject constructor(
uri = uri,
info = MediaInfo(
mimeType = resolvedMimeType,
name = fileName,
filename = fileName,
caption = caption,
formattedFileSize = fileSize,
fileExtension = fileExtension
)

3
libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt

@ -29,7 +29,8 @@ class AndroidLocalMediaFactoryTest { @@ -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",

3
libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt

@ -32,7 +32,8 @@ class FakeLocalMediaFactory( @@ -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)

12
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt

@ -265,15 +265,15 @@ class DefaultNotifiableEventResolver @Inject constructor( @@ -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( @@ -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

2
libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationMediaRepo.kt

@ -47,7 +47,7 @@ interface NotificationMediaRepo { @@ -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(

10
libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt

@ -187,7 +187,7 @@ class DefaultNotifiableEventResolverTest { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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),
),
)
)

4
tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_SheetContent_Day_3_en.png

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c13e631e41cafa49321988065bedff6081fbc4aa99d9cb3a32b4d860888c7535
size 42412
oid sha256:0f5adfb435286a84587a66d6970b1a7f03a8cb53eac86956f61092d97ea16951
size 43121

4
tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_SheetContent_Night_3_en.png

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:35988d035ddf2eea2463586024703fc630f996c6e03a232fc65196ab5186b89a
size 41582
oid sha256:87d68af6f69c67de710145b3fabbff40bc6c9a767542e5a62be80d084972cb28
size 42221

Loading…
Cancel
Save