Browse Source

Introduce MatrixMediaSource

feature/jme/open-room-member-details-when-clicking-on-user-data
ganfra 1 year ago
parent
commit
4236b69705
  1. 5
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt
  2. 17
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt
  3. 81
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt
  4. 30
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt
  5. 8
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt
  6. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt
  7. 33
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt
  8. 42
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt
  9. 10
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/DefaultRoomLastMessageFormatterTests.kt
  10. 2
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/FileInfo.kt
  11. 2
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/ImageInfo.kt
  12. 10
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt
  13. 25
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaSource.kt
  14. 2
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt
  15. 13
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt
  16. 4
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/FileInfo.kt
  17. 2
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/ImageInfo.kt
  18. 5
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaSource.kt
  19. 20
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt
  20. 2
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/VideoInfo.kt
  21. 9
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt
  22. 4
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt
  23. 13
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt
  24. 9
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt
  25. 12
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt
  26. 4
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt
  27. 7
      libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt

5
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt

@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
private fun Modifier.defaultContentPadding(): Modifier = padding( private fun Modifier.defaultContentPadding(): Modifier = padding(
horizontal = 12.dp, vertical = 6.dp horizontal = 12.dp, vertical = 6.dp
@ -64,5 +65,9 @@ fun TimelineItemEventContentView(
content = content, content = content,
modifier = modifier, modifier = modifier,
) )
is TimelineItemVideoContent -> TimelineItemVideoView(
content = content,
modifier = modifier
)
} }
} }

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

@ -18,39 +18,38 @@ package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider
import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.matrix.ui.media.MediaRequestData
import kotlin.math.min
@Composable @Composable
fun TimelineItemImageView( fun TimelineItemImageView(
content: TimelineItemImageContent, content: TimelineItemImageContent,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val widthPercent = if (content.aspectRatio > 1f) { val maxHeight = min(300, content.height ?: Int.MAX_VALUE)
1f
} else {
0.7f
}
Box( Box(
modifier = modifier modifier = modifier
.fillMaxWidth(widthPercent) .heightIn(max = maxHeight.dp)
.aspectRatio(content.aspectRatio), .aspectRatio(content.aspectRatio),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
BlurHashAsyncImage( BlurHashAsyncImage(
blurHash = content.blurhash, blurHash = content.blurhash,
model = content.mediaRequestData, model = MediaRequestData(content.mediaSource, MediaRequestData.Kind.Content),
contentScale = ContentScale.Crop, contentScale = ContentScale.Fit,
) )
} }
} }

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

@ -0,0 +1,81 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.components.event
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.heightIn
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.matrix.ui.media.MediaRequestData
import kotlin.math.min
@Composable
fun TimelineItemVideoView(
content: TimelineItemVideoContent,
modifier: Modifier = Modifier,
) {
val maxHeight = min(300, content.height ?: Int.MAX_VALUE)
Box(
modifier = modifier
.heightIn(max = maxHeight.dp)
.aspectRatio(content.aspectRatio),
contentAlignment = Alignment.Center,
) {
BlurHashAsyncImage(
blurHash = content.blurhash,
model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.Content),
contentScale = ContentScale.Fit,
)
Image(
painterResource(id = androidx.media3.ui.R.drawable.exo_ic_play_circle_filled),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
)
}
}
@Preview
@Composable
internal fun TimelineItemVideoViewLightPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) =
ElementPreviewLight { ContentToPreview(content) }
@Preview
@Composable
internal fun TimelineItemVideoViewDarkPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) =
ElementPreviewDark { ContentToPreview(content) }
@Composable
private fun ContentToPreview(content: TimelineItemImageContent) {
TimelineItemImageView(content)
}

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

@ -22,13 +22,14 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
import io.element.android.features.messages.impl.timeline.util.toHtmlDocument import io.element.android.features.messages.impl.timeline.util.toHtmlDocument
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import javax.inject.Inject import javax.inject.Inject
class TimelineItemContentMessageFactory @Inject constructor() { class TimelineItemContentMessageFactory @Inject constructor() {
@ -49,10 +50,29 @@ class TimelineItemContentMessageFactory @Inject constructor() {
} }
TimelineItemImageContent( TimelineItemImageContent(
body = messageType.body, body = messageType.body,
mediaRequestData = MediaRequestData( height = messageType.info?.height?.toInt(),
url = messageType.url, width = messageType.info?.width?.toInt(),
kind = MediaRequestData.Kind.Content mediaSource = messageType.source,
), blurhash = messageType.info?.blurhash,
aspectRatio = aspectRatio
)
}
is VideoMessageType -> {
val height = messageType.info?.height?.toFloat()
val width = messageType.info?.width?.toFloat()
val aspectRatio = if (height != null && width != null) {
width / height
} else {
0.7f
}
TimelineItemVideoContent(
body = messageType.body,
thumbnailSource = messageType.info?.thumbnailSource,
videoSource = messageType.source,
mimetype = messageType.info?.mimetype,
width = messageType.info?.width?.toInt(),
height = messageType.info?.height?.toInt(),
duration = messageType.info?.duration ?: 0L,
blurhash = messageType.info?.blurhash, blurhash = messageType.info?.blurhash,
aspectRatio = aspectRatio aspectRatio = aspectRatio
) )

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

@ -16,13 +16,15 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.api.media.MatrixMediaSource
data class TimelineItemImageContent( data class TimelineItemImageContent(
val body: String, val body: String,
val mediaRequestData: MediaRequestData, val mediaSource: MatrixMediaSource,
val blurhash: String?, val blurhash: String?,
val width: Int?,
val height: Int?,
val aspectRatio: Float val aspectRatio: Float
) : TimelineItemEventContent{ ) : TimelineItemEventContent {
override val type: String = "TimelineItemImageContent" override val type: String = "TimelineItemImageContent"
} }

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

@ -17,6 +17,7 @@
package io.element.android.features.messages.impl.timeline.model.event package io.element.android.features.messages.impl.timeline.model.event
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import io.element.android.libraries.matrix.ui.media.MediaRequestData import io.element.android.libraries.matrix.ui.media.MediaRequestData
open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineItemImageContent> { open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineItemImageContent> {
@ -30,7 +31,9 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineI
fun aTimelineItemImageContent() = TimelineItemImageContent( fun aTimelineItemImageContent() = TimelineItemImageContent(
body = "a body", body = "a body",
mediaRequestData = MediaRequestData(url = "", kind = MediaRequestData.Kind.Content), mediaSource = MatrixMediaSource(""),
blurhash = null, blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
aspectRatio = 0.5f, aspectRatio = 0.5f,
height = null,
width = null
) )

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

@ -0,0 +1,33 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.model.event
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
data class TimelineItemVideoContent(
val body: String,
val duration: Long,
val videoSource: MatrixMediaSource,
val thumbnailSource: MatrixMediaSource?,
val aspectRatio: Float,
val blurhash: String?,
val height: Int?,
val width: Int?,
val mimetype: String?,
) : TimelineItemEventContent {
override val type: String = "TimelineItemImageContent"
}

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

@ -0,0 +1,42 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.timeline.model.event
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import io.element.android.libraries.matrix.ui.media.MediaRequestData
open class TimelineItemVideoContentProvider : PreviewParameterProvider<TimelineItemVideoContent> {
override val values: Sequence<TimelineItemVideoContent>
get() = sequenceOf(
aTimelineItemVideoContent(),
aTimelineItemVideoContent().copy(aspectRatio = 1.0f),
aTimelineItemVideoContent().copy(aspectRatio = 1.5f),
)
}
fun aTimelineItemVideoContent() = TimelineItemVideoContent(
body = "a video",
thumbnailSource = MatrixMediaSource(url = ""),
blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
aspectRatio = 0.5f,
duration = 0,
videoSource = MatrixMediaSource(""),
height = null,
width = null,
mimetype = null
)

10
features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/DefaultRoomLastMessageFormatterTests.kt

@ -21,6 +21,7 @@ import androidx.compose.ui.text.AnnotatedString
import com.google.common.truth.Truth import com.google.common.truth.Truth
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
@ -145,12 +146,13 @@ class DefaultRoomLastMessageFormatterTests {
fun createMessageContent(type: MessageType): MessageContent { fun createMessageContent(type: MessageType): MessageContent {
return MessageContent(body, null, false, type) return MessageContent(body, null, false, type)
} }
val sharedContentMessagesTypes = arrayOf( val sharedContentMessagesTypes = arrayOf(
TextMessageType(body, null), TextMessageType(body, null),
VideoMessageType(body, "url", null), VideoMessageType(body, MatrixMediaSource("url"), null),
AudioMessageType(body, "url", null), AudioMessageType(body, MatrixMediaSource("url"), null),
ImageMessageType(body, "url", null), ImageMessageType(body, MatrixMediaSource("url"), null),
FileMessageType(body, "url", null), FileMessageType(body, MatrixMediaSource("url"), null),
NoticeMessageType(body, null), NoticeMessageType(body, null),
EmoteMessageType(body, null), EmoteMessageType(body, null),
) )

2
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/FileInfo.kt

@ -20,5 +20,5 @@ data class FileInfo(
val mimetype: String?, val mimetype: String?,
val size: Long?, val size: Long?,
val thumbnailInfo: ThumbnailInfo?, val thumbnailInfo: ThumbnailInfo?,
val thumbnailUrl: String? val thumbnailSource: MatrixMediaSource?
) )

2
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/ImageInfo.kt

@ -22,6 +22,6 @@ data class ImageInfo(
val mimetype: String?, val mimetype: String?,
val size: Long?, val size: Long?,
val thumbnailInfo: ThumbnailInfo?, val thumbnailInfo: ThumbnailInfo?,
val thumbnailUrl: String?, val thumbnailSource: MatrixMediaSource?,
val blurhash: String? val blurhash: String?
) )

10
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt

@ -16,14 +16,14 @@
package io.element.android.libraries.matrix.api.media package io.element.android.libraries.matrix.api.media
import java.nio.file.Path import android.net.Uri
interface MatrixMediaLoader { interface MatrixMediaLoader {
/** /**
* @param url to fetch the content for. * @param url to fetch the content for.
* @return a [Result] of ByteArray. It contains the binary data for the media. * @return a [Result] of ByteArray. It contains the binary data for the media.
*/ */
suspend fun loadMediaContent(url: String): Result<ByteArray> suspend fun loadMediaContent(source: MatrixMediaSource): Result<ByteArray>
/** /**
* @param url to fetch the data for. * @param url to fetch the data for.
@ -31,12 +31,12 @@ interface MatrixMediaLoader {
* @param height: the desired height for rescaling the media as thumbnail * @param height: the desired height for rescaling the media as thumbnail
* @return a [Result] of ByteArray. It contains the binary data for the media. * @return a [Result] of ByteArray. It contains the binary data for the media.
*/ */
suspend fun loadMediaThumbnail(url: String, width: Long, height: Long): Result<ByteArray> suspend fun loadMediaThumbnail(source: MatrixMediaSource, width: Long, height: Long): Result<ByteArray>
/** /**
* @param url to fetch the data for. * @param url to fetch the data for.
* @param mimeType: optional mime type * @param mimeType: optional mime type
* @return a [Result] of [Path]. It's the path to the downloaded file. * @return a [Result] of [Uri]. It's the uri of the downloaded file.
*/ */
suspend fun loadMediaFile(url: String, mimeType: String?): Result<Path> suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<Uri>
} }

25
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaSource.kt

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.media
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class MatrixMediaSource(
val url: String
) : Parcelable

2
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt

@ -23,6 +23,6 @@ data class VideoInfo(
val mimetype: String?, val mimetype: String?,
val size: Long?, val size: Long?,
val thumbnailInfo: ThumbnailInfo?, val thumbnailInfo: ThumbnailInfo?,
val thumbnailUrl: String?, val thumbnailSource: MatrixMediaSource?,
val blurhash: String? val blurhash: String?
) )

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

@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.media.VideoInfo
sealed interface EventContent sealed interface EventContent
@ -106,25 +107,25 @@ data class EmoteMessageType(
data class ImageMessageType( data class ImageMessageType(
val body: String, val body: String,
val url: String, val source: MatrixMediaSource,
val info: ImageInfo? val info: ImageInfo?
) : MessageType ) : MessageType
data class AudioMessageType( data class AudioMessageType(
var body: String, val body: String,
var url: String, val source: MatrixMediaSource,
var info: AudioInfo? val info: AudioInfo?
) : MessageType ) : MessageType
data class VideoMessageType( data class VideoMessageType(
val body: String, val body: String,
val url: String, val source: MatrixMediaSource,
val info: VideoInfo? val info: VideoInfo?
) : MessageType ) : MessageType
data class FileMessageType( data class FileMessageType(
val body: String, val body: String,
val url: String, val source: MatrixMediaSource,
val info: FileInfo? val info: FileInfo?
) : MessageType ) : MessageType

4
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/FileInfo.kt

@ -17,13 +17,11 @@
package io.element.android.libraries.matrix.impl.media package io.element.android.libraries.matrix.impl.media
import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ThumbnailInfo
import org.matrix.rustcomponents.sdk.FileInfo as RustFileInfo import org.matrix.rustcomponents.sdk.FileInfo as RustFileInfo
import org.matrix.rustcomponents.sdk.ThumbnailInfo as RustThumbnailInfo
fun RustFileInfo.map(): FileInfo = FileInfo( fun RustFileInfo.map(): FileInfo = FileInfo(
mimetype = mimetype, mimetype = mimetype,
size = size?.toLong(), size = size?.toLong(),
thumbnailInfo = thumbnailInfo?.map(), thumbnailInfo = thumbnailInfo?.map(),
thumbnailUrl = thumbnailSource?.useUrl() thumbnailSource = thumbnailSource?.map()
) )

2
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/ImageInfo.kt

@ -25,6 +25,6 @@ fun RustImageInfo.map(): ImageInfo = ImageInfo(
mimetype = mimetype, mimetype = mimetype,
size = size?.toLong(), size = size?.toLong(),
thumbnailInfo = thumbnailInfo?.map(), thumbnailInfo = thumbnailInfo?.map(),
thumbnailUrl = thumbnailSource?.useUrl(), thumbnailSource = thumbnailSource?.map(),
blurhash = blurhash blurhash = blurhash
) )

5
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaSource.kt

@ -16,7 +16,8 @@
package io.element.android.libraries.matrix.impl.media package io.element.android.libraries.matrix.impl.media
import org.matrix.rustcomponents.sdk.MediaSource import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource
fun MediaSource.useUrl(): String = use { it.url() } fun RustMediaSource.map(): MatrixMediaSource = use { MatrixMediaSource(it.url()) }

20
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt

@ -16,14 +16,15 @@
package io.element.android.libraries.matrix.impl.media package io.element.android.libraries.matrix.impl.media
import android.net.Uri
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import java.nio.file.Path import java.io.File
import kotlin.io.path.Path
class RustMediaLoader( class RustMediaLoader(
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
@ -31,10 +32,10 @@ class RustMediaLoader(
) : MatrixMediaLoader { ) : MatrixMediaLoader {
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
override suspend fun loadMediaContent(url: String): Result<ByteArray> = override suspend fun loadMediaContent(source: MatrixMediaSource): Result<ByteArray> =
withContext(dispatchers.io) { withContext(dispatchers.io) {
runCatching { runCatching {
mediaSourceFromUrl(url).use { source -> mediaSourceFromUrl(source.url).use { source ->
innerClient.getMediaContent(source).toUByteArray().toByteArray() innerClient.getMediaContent(source).toUByteArray().toByteArray()
} }
} }
@ -42,13 +43,13 @@ class RustMediaLoader(
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
override suspend fun loadMediaThumbnail( override suspend fun loadMediaThumbnail(
url: String, source: MatrixMediaSource,
width: Long, width: Long,
height: Long height: Long
): Result<ByteArray> = ): Result<ByteArray> =
withContext(dispatchers.io) { withContext(dispatchers.io) {
runCatching { runCatching {
mediaSourceFromUrl(url).use { mediaSource -> mediaSourceFromUrl(source.url).use { mediaSource ->
innerClient.getMediaThumbnail( innerClient.getMediaThumbnail(
mediaSource = mediaSource, mediaSource = mediaSource,
width = width.toULong(), width = width.toULong(),
@ -58,15 +59,16 @@ class RustMediaLoader(
} }
} }
override suspend fun loadMediaFile(url: String, mimeType: String?): Result<Path> = override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<Uri> =
withContext(dispatchers.io) { withContext(dispatchers.io) {
runCatching { runCatching {
mediaSourceFromUrl(url).use { mediaSource -> mediaSourceFromUrl(source.url).use { mediaSource ->
innerClient.getMediaFile( innerClient.getMediaFile(
mediaSource = mediaSource, mediaSource = mediaSource,
mimeType = mimeType ?: "application/octet-stream" mimeType = mimeType ?: "application/octet-stream"
).use { ).use {
Path(it.path()) val file = File(it.path())
Uri.fromFile(file)
} }
} }
} }

2
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/VideoInfo.kt

@ -26,6 +26,6 @@ fun RustVideoInfo.map(): VideoInfo = VideoInfo(
mimetype = mimetype, mimetype = mimetype,
size = size?.toLong(), size = size?.toLong(),
thumbnailInfo = thumbnailInfo?.map(), thumbnailInfo = thumbnailInfo?.map(),
thumbnailUrl = thumbnailSource?.useUrl(), thumbnailSource = thumbnailSource?.map(),
blurhash = blurhash blurhash = blurhash
) )

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

@ -29,7 +29,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.media.useUrl
import org.matrix.rustcomponents.sdk.Message import org.matrix.rustcomponents.sdk.Message
import org.matrix.rustcomponents.sdk.MessageType import org.matrix.rustcomponents.sdk.MessageType
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
@ -42,13 +41,13 @@ class EventMessageMapper {
val type = message.msgtype().use { type -> val type = message.msgtype().use { type ->
when (type) { when (type) {
is MessageType.Audio -> { is MessageType.Audio -> {
AudioMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map()) AudioMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
} }
is MessageType.File -> { is MessageType.File -> {
FileMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map()) FileMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
} }
is MessageType.Image -> { is MessageType.Image -> {
ImageMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map()) ImageMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
} }
is MessageType.Notice -> { is MessageType.Notice -> {
NoticeMessageType(type.content.body, type.content.formatted?.map()) NoticeMessageType(type.content.body, type.content.formatted?.map())
@ -60,7 +59,7 @@ class EventMessageMapper {
EmoteMessageType(type.content.body, type.content.formatted?.map()) EmoteMessageType(type.content.body, type.content.formatted?.map())
} }
is MessageType.Video -> { is MessageType.Video -> {
VideoMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map()) VideoMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
} }
null -> { null -> {
UnknownMessageType UnknownMessageType

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

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.impl.timeline.item.event package io.element.android.libraries.matrix.impl.timeline.item.event
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
@ -26,7 +27,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RedactedConte
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.media.map
@ -88,7 +88,7 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
StickerContent( StickerContent(
body = kind.body, body = kind.body,
info = kind.info.map(), info = kind.info.map(),
url = kind.url url = kind.url,
) )
} }
is TimelineItemContentKind.UnableToDecrypt -> { is TimelineItemContentKind.UnableToDecrypt -> {

13
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt

@ -16,15 +16,16 @@
package io.element.android.libraries.matrix.test.media package io.element.android.libraries.matrix.test.media
import android.net.Uri
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import java.nio.file.Path import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import kotlin.io.path.Path import java.io.File
class FakeMediaLoader : MatrixMediaLoader { class FakeMediaLoader : MatrixMediaLoader {
var shouldFail = false var shouldFail = false
override suspend fun loadMediaContent(url: String): Result<ByteArray> { override suspend fun loadMediaContent(source: MatrixMediaSource): Result<ByteArray> {
return if (shouldFail) { return if (shouldFail) {
Result.failure(RuntimeException()) Result.failure(RuntimeException())
} else { } else {
@ -32,7 +33,7 @@ class FakeMediaLoader : MatrixMediaLoader {
} }
} }
override suspend fun loadMediaThumbnail(url: String, width: Long, height: Long): Result<ByteArray> { override suspend fun loadMediaThumbnail(source: MatrixMediaSource, width: Long, height: Long): Result<ByteArray> {
return if (shouldFail) { return if (shouldFail) {
Result.failure(RuntimeException()) Result.failure(RuntimeException())
} else { } else {
@ -40,11 +41,11 @@ class FakeMediaLoader : MatrixMediaLoader {
} }
} }
override suspend fun loadMediaFile(url: String, mimeType: String?): Result<Path> { override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<Uri> {
return if (shouldFail) { return if (shouldFail) {
Result.failure(RuntimeException()) Result.failure(RuntimeException())
} else { } else {
return Result.success(Path("path")) return Result.success(Uri.fromFile(File("path")))
} }
} }
} }

9
libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt

@ -17,13 +17,12 @@
package io.element.android.libraries.matrix.ui.media package io.element.android.libraries.matrix.ui.media
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import kotlin.math.roundToLong import kotlin.math.roundToLong
fun AvatarData.toMediaRequestData(): MediaRequestData? { fun AvatarData.toMediaRequestData(): MediaRequestData {
return url?.let { return MediaRequestData(
MediaRequestData( source = url?.let { MatrixMediaSource(it) },
url = it,
kind = MediaRequestData.Kind.Thumbnail(size.dp.value.roundToLong()) kind = MediaRequestData.Kind.Thumbnail(size.dp.value.roundToLong())
) )
}
} }

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

@ -38,19 +38,15 @@ internal class CoilMediaFetcher(
ByteBuffer.wrap(data) ByteBuffer.wrap(data)
}.map { byteBuffer -> }.map { byteBuffer ->
imageLoader.components.newFetcher(byteBuffer, options, imageLoader)?.first?.fetch() imageLoader.components.newFetcher(byteBuffer, options, imageLoader)?.first?.fetch()
} }.getOrThrow()
.fold(
{ result -> result },
{ failure -> throw failure }
)
} }
private suspend fun loadMedia(): Result<ByteArray> { private suspend fun loadMedia(): Result<ByteArray> {
if (mediaData == null) return Result.failure(IllegalStateException("No media data to fetch.")) if (mediaData?.source == null) return Result.failure(IllegalStateException("No media data to fetch."))
return when (mediaData.kind) { return when (mediaData.kind) {
is MediaRequestData.Kind.Content -> mediaLoader.loadMediaContent(url = mediaData.url) is MediaRequestData.Kind.Content -> mediaLoader.loadMediaContent(source = mediaData.source)
is MediaRequestData.Kind.Thumbnail -> mediaLoader.loadMediaThumbnail( is MediaRequestData.Kind.Thumbnail -> mediaLoader.loadMediaThumbnail(
url = mediaData.url, source = mediaData.source,
width = mediaData.kind.width, width = mediaData.kind.width,
height = mediaData.kind.height height = mediaData.kind.height
) )

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

@ -16,8 +16,10 @@
package io.element.android.libraries.matrix.ui.media package io.element.android.libraries.matrix.ui.media
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
data class MediaRequestData( data class MediaRequestData(
val url: String, val source: MatrixMediaSource?,
val kind: Kind val kind: Kind
) { ) {

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

@ -22,7 +22,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
internal class AvatarDataKeyer : Keyer<AvatarData> { internal class AvatarDataKeyer : Keyer<AvatarData> {
override fun key(data: AvatarData, options: Options): String? { override fun key(data: AvatarData, options: Options): String? {
return data.toMediaRequestData()?.toKey() return data.toMediaRequestData().toKey()
} }
} }
@ -32,4 +32,7 @@ internal class MediaRequestDataKeyer : Keyer<MediaRequestData> {
} }
} }
private fun MediaRequestData.toKey() = "${url}_${kind}" private fun MediaRequestData.toKey(): String? {
if (source == null) return null
return "${source.url}_${kind}"
}

Loading…
Cancel
Save