Introduce MatrixMediaSource

This commit is contained in:
ganfra 2023-05-05 19:47:10 +02:00
parent affb928801
commit 72dea2f817
27 changed files with 298 additions and 84 deletions

View file

@ -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
)
} }
} }

View file

@ -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,
) )
} }
} }

View file

@ -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)
}

View file

@ -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
) )

View file

@ -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"
} }

View file

@ -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
) )

View file

@ -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"
}

View file

@ -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
)

View file

@ -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),
) )

View file

@ -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?
) )

View file

@ -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?
) )

View file

@ -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>
} }

View file

@ -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

View file

@ -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?
) )

View file

@ -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

View file

@ -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()
) )

View file

@ -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
) )

View file

@ -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()) }

View file

@ -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)
} }
} }
} }

View file

@ -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
) )

View file

@ -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

View file

@ -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 -> {

View file

@ -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")))
} }
} }
} }

View file

@ -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()) )
)
}
} }

View file

@ -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
) )

View file

@ -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
) { ) {

View file

@ -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}"
}