Media: improve media viewer
This commit is contained in:
parent
fe246e0a7b
commit
c191d5991d
20 changed files with 244 additions and 59 deletions
|
|
@ -62,7 +62,11 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
object Messages : NavTarget
|
object Messages : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class MediaViewer(val title: String, val mediaSource: MatrixMediaSource) : NavTarget
|
data class MediaViewer(
|
||||||
|
val title: String,
|
||||||
|
val mediaSource: MatrixMediaSource,
|
||||||
|
val mimeType: String?
|
||||||
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AttachmentPreview(val attachment: Attachment) : NavTarget
|
data class AttachmentPreview(val attachment: Attachment) : NavTarget
|
||||||
|
|
@ -89,7 +93,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
createNode<MessagesNode>(buildContext, listOf(callback))
|
createNode<MessagesNode>(buildContext, listOf(callback))
|
||||||
}
|
}
|
||||||
is NavTarget.MediaViewer -> {
|
is NavTarget.MediaViewer -> {
|
||||||
val inputs = MediaViewerNode.Inputs(navTarget.title, navTarget.mediaSource)
|
val inputs = MediaViewerNode.Inputs(navTarget.title, navTarget.mediaSource, navTarget.mimeType)
|
||||||
createNode<MediaViewerNode>(buildContext, listOf(inputs))
|
createNode<MediaViewerNode>(buildContext, listOf(inputs))
|
||||||
}
|
}
|
||||||
is NavTarget.AttachmentPreview -> {
|
is NavTarget.AttachmentPreview -> {
|
||||||
|
|
@ -103,12 +107,12 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
when (event.content) {
|
when (event.content) {
|
||||||
is TimelineItemImageContent -> {
|
is TimelineItemImageContent -> {
|
||||||
val mediaSource = event.content.mediaSource
|
val mediaSource = event.content.mediaSource
|
||||||
val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource)
|
val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource, event.content.mimeType)
|
||||||
backstack.push(navTarget)
|
backstack.push(navTarget)
|
||||||
}
|
}
|
||||||
is TimelineItemVideoContent -> {
|
is TimelineItemVideoContent -> {
|
||||||
val mediaSource = event.content.videoSource
|
val mediaSource = event.content.videoSource
|
||||||
val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource)
|
val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource, event.content.mimeType)
|
||||||
backstack.push(navTarget)
|
backstack.push(navTarget)
|
||||||
}
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.media.local
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class LocalMedia(
|
data class LocalMedia(
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
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
|
||||||
|
|
@ -84,13 +86,17 @@ fun MediaVideoView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val exoPlayer = ExoPlayer.Builder(LocalContext.current).build()
|
val exoPlayer = remember {
|
||||||
val mediaItem = MediaItem.Builder()
|
ExoPlayer.Builder(context).build()
|
||||||
.setUri(uri)
|
.apply {
|
||||||
.build()
|
this.playWhenReady = true
|
||||||
exoPlayer.playWhenReady
|
this.prepare()
|
||||||
exoPlayer.setMediaItem(mediaItem)
|
}
|
||||||
exoPlayer.prepare()
|
}
|
||||||
|
LaunchedEffect(uri) {
|
||||||
|
val mediaItem = MediaItem.fromUri(uri)
|
||||||
|
exoPlayer.setMediaItem(mediaItem)
|
||||||
|
}
|
||||||
|
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = {
|
factory = {
|
||||||
|
|
@ -98,8 +104,10 @@ fun MediaVideoView(
|
||||||
player = exoPlayer
|
player = exoPlayer
|
||||||
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
|
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||||
|
controllerShowTimeoutMs = 3000
|
||||||
}
|
}
|
||||||
}, modifier = modifier.fillMaxSize()
|
},
|
||||||
|
modifier = modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
|
|
||||||
OnLifecycleEvent { _, event ->
|
OnLifecycleEvent { _, event ->
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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.media.viewer
|
||||||
|
|
||||||
|
sealed interface MediaViewerEvents {
|
||||||
|
object RetryLoading : MediaViewerEvents
|
||||||
|
object SaveOnDisk : MediaViewerEvents
|
||||||
|
}
|
||||||
|
|
@ -39,11 +39,12 @@ class MediaViewerNode @AssistedInject constructor(
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val name: String,
|
val name: String,
|
||||||
val mediaSource: MatrixMediaSource,
|
val mediaSource: MatrixMediaSource,
|
||||||
|
val mimeType: String?
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
|
|
||||||
private val inputs: Inputs = inputs()
|
private val inputs: Inputs = inputs()
|
||||||
|
|
||||||
private val presenter = presenterFactory.create(inputs.name, inputs.mediaSource)
|
private val presenter = presenterFactory.create(inputs)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,14 @@
|
||||||
package io.element.android.features.messages.impl.media.viewer
|
package io.element.android.features.messages.impl.media.viewer
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.core.net.toUri
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
|
@ -27,37 +33,65 @@ import io.element.android.features.messages.impl.media.local.LocalMediaFactory
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
|
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MediaViewerPresenter @AssistedInject constructor(
|
class MediaViewerPresenter @AssistedInject constructor(
|
||||||
@Assisted private val name: String,
|
@Assisted private val inputs: MediaViewerNode.Inputs,
|
||||||
@Assisted private val mediaSource: MatrixMediaSource,
|
|
||||||
private val localMediaFactory: LocalMediaFactory,
|
private val localMediaFactory: LocalMediaFactory,
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
) : Presenter<MediaViewerState> {
|
) : Presenter<MediaViewerState> {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(name: String, mediaSource: MatrixMediaSource): MediaViewerPresenter
|
fun create(inputs: MediaViewerNode.Inputs): MediaViewerPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): MediaViewerState {
|
override fun present(): MediaViewerState {
|
||||||
val localMedia by produceState<Async<LocalMedia>>(initialValue = Async.Uninitialized) {
|
val coroutineScope = rememberCoroutineScope()
|
||||||
value = Async.Loading(null)
|
var loadMediaTrigger by remember { mutableStateOf(0) }
|
||||||
//TODO we are missing some permissions to use this API
|
val mediaFile: MutableState<MediaFile?> = remember {
|
||||||
client.mediaLoader.loadMediaFile(mediaSource, null)
|
mutableStateOf(null)
|
||||||
.onSuccess {
|
}
|
||||||
val localMedia = localMediaFactory.createFromUri(uri = it, null)
|
val localMedia: MutableState<Async<LocalMedia>> = remember {
|
||||||
Async.Success(localMedia)
|
mutableStateOf(Async.Uninitialized)
|
||||||
}.onFailure {
|
}
|
||||||
Async.Failure(it, null)
|
DisposableEffect(loadMediaTrigger) {
|
||||||
}
|
coroutineScope.loadMedia(mediaFile, localMedia)
|
||||||
|
onDispose {
|
||||||
|
mediaFile.value?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEvents(mediaViewerEvents: MediaViewerEvents) {
|
||||||
|
when (mediaViewerEvents) {
|
||||||
|
MediaViewerEvents.RetryLoading -> loadMediaTrigger++
|
||||||
|
MediaViewerEvents.SaveOnDisk -> TODO()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return MediaViewerState(
|
return MediaViewerState(
|
||||||
name = name,
|
name = inputs.name,
|
||||||
downloadedMedia = localMedia,
|
downloadedMedia = localMedia.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.loadMedia(mediaFile: MutableState<MediaFile?>, localMedia: MutableState<Async<LocalMedia>>) = launch {
|
||||||
|
mediaFile.value = null
|
||||||
|
localMedia.value = Async.Loading()
|
||||||
|
client.mediaLoader.loadMediaFile(inputs.mediaSource, inputs.mimeType)
|
||||||
|
.onSuccess {
|
||||||
|
mediaFile.value = it
|
||||||
|
}.mapCatching {
|
||||||
|
val uri = it.path().toUri()
|
||||||
|
localMediaFactory.createFromUri(uri, inputs.mimeType)!!
|
||||||
|
}.onSuccess {
|
||||||
|
localMedia.value = Async.Success(it)
|
||||||
|
}.onFailure {
|
||||||
|
localMedia.value = Async.Failure(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,6 @@ import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
data class MediaViewerState(
|
data class MediaViewerState(
|
||||||
val name: String,
|
val name: String,
|
||||||
val downloadedMedia: Async<LocalMedia>
|
val downloadedMedia: Async<LocalMedia>,
|
||||||
|
val eventSink: (MediaViewerEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -29,5 +29,6 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
|
||||||
|
|
||||||
fun aMediaViewerState() = MediaViewerState(
|
fun aMediaViewerState() = MediaViewerState(
|
||||||
name = "A media",
|
name = "A media",
|
||||||
downloadedMedia = Async.Uninitialized
|
downloadedMedia = Async.Uninitialized,
|
||||||
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,37 +19,50 @@
|
||||||
package io.element.android.features.messages.impl.media.viewer
|
package io.element.android.features.messages.impl.media.viewer
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
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.res.stringResource
|
||||||
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 androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.messages.impl.media.local.LocalMediaView
|
import io.element.android.features.messages.impl.media.local.LocalMediaView
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
|
|
||||||
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.designsystem.theme.components.Button
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.ui.strings.R.string as StringR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MediaViewerView(
|
fun MediaViewerView(
|
||||||
state: MediaViewerState,
|
state: MediaViewerState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
fun onRetry() {
|
||||||
|
state.eventSink(MediaViewerEvents.RetryLoading)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(modifier) {
|
Scaffold(modifier) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(it),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
when (state.downloadedMedia) {
|
when (state.downloadedMedia) {
|
||||||
is Async.Success -> LocalMediaView(state.downloadedMedia.state)
|
is Async.Success -> LocalMediaView(state.downloadedMedia.state)
|
||||||
is Async.Failure -> ErrorDialog(
|
is Async.Failure -> ErrorView("Error while downloading", ::onRetry)
|
||||||
content = "Error while downloading the media",
|
|
||||||
)
|
|
||||||
else -> CircularProgressIndicator(
|
else -> CircularProgressIndicator(
|
||||||
strokeWidth = 2.dp,
|
strokeWidth = 2.dp,
|
||||||
)
|
)
|
||||||
|
|
@ -58,6 +71,27 @@ fun MediaViewerView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ErrorView(
|
||||||
|
errorMessage: String,
|
||||||
|
onRetry: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text(text = errorMessage)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Button(
|
||||||
|
onClick = onRetry
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = StringR.action_retry))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun MediaViewerViewLightPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) =
|
fun MediaViewerViewLightPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) =
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ class TimelineItemContentMessageFactory @Inject constructor() {
|
||||||
body = messageType.body,
|
body = messageType.body,
|
||||||
height = messageType.info?.height?.toInt(),
|
height = messageType.info?.height?.toInt(),
|
||||||
width = messageType.info?.width?.toInt(),
|
width = messageType.info?.width?.toInt(),
|
||||||
|
mimeType = messageType.info?.mimetype,
|
||||||
mediaSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
blurhash = messageType.info?.blurhash,
|
blurhash = messageType.info?.blurhash,
|
||||||
aspectRatio = aspectRatio
|
aspectRatio = aspectRatio
|
||||||
|
|
@ -69,7 +70,7 @@ class TimelineItemContentMessageFactory @Inject constructor() {
|
||||||
body = messageType.body,
|
body = messageType.body,
|
||||||
thumbnailSource = messageType.info?.thumbnailSource,
|
thumbnailSource = messageType.info?.thumbnailSource,
|
||||||
videoSource = messageType.source,
|
videoSource = messageType.source,
|
||||||
mimetype = messageType.info?.mimetype,
|
mimeType = messageType.info?.mimetype,
|
||||||
width = messageType.info?.width?.toInt(),
|
width = messageType.info?.width?.toInt(),
|
||||||
height = messageType.info?.height?.toInt(),
|
height = messageType.info?.height?.toInt(),
|
||||||
duration = messageType.info?.duration ?: 0L,
|
duration = messageType.info?.duration ?: 0L,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.media.MatrixMediaSource
|
||||||
data class TimelineItemImageContent(
|
data class TimelineItemImageContent(
|
||||||
val body: String,
|
val body: String,
|
||||||
val mediaSource: MatrixMediaSource,
|
val mediaSource: MatrixMediaSource,
|
||||||
|
val mimeType: String?,
|
||||||
val blurhash: String?,
|
val blurhash: String?,
|
||||||
val width: Int?,
|
val width: Int?,
|
||||||
val height: Int?,
|
val height: Int?,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ fun aTimelineItemImageContent() = TimelineItemImageContent(
|
||||||
mediaSource = MatrixMediaSource(""),
|
mediaSource = MatrixMediaSource(""),
|
||||||
blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
|
blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
|
||||||
aspectRatio = 0.5f,
|
aspectRatio = 0.5f,
|
||||||
|
mimeType = "null",
|
||||||
height = null,
|
height = null,
|
||||||
width = null
|
width = null
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ data class TimelineItemVideoContent(
|
||||||
val blurhash: String?,
|
val blurhash: String?,
|
||||||
val height: Int?,
|
val height: Int?,
|
||||||
val width: Int?,
|
val width: Int?,
|
||||||
val mimetype: String?,
|
val mimeType: String?,
|
||||||
) : TimelineItemEventContent {
|
) : TimelineItemEventContent {
|
||||||
override val type: String = "TimelineItemImageContent"
|
override val type: String = "TimelineItemImageContent"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ 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.api.media.MatrixMediaSource
|
||||||
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
|
||||||
|
|
||||||
open class TimelineItemVideoContentProvider : PreviewParameterProvider<TimelineItemVideoContent> {
|
open class TimelineItemVideoContentProvider : PreviewParameterProvider<TimelineItemVideoContent> {
|
||||||
override val values: Sequence<TimelineItemVideoContent>
|
override val values: Sequence<TimelineItemVideoContent>
|
||||||
|
|
@ -38,5 +37,5 @@ fun aTimelineItemVideoContent() = TimelineItemVideoContent(
|
||||||
videoSource = MatrixMediaSource(""),
|
videoSource = MatrixMediaSource(""),
|
||||||
height = null,
|
height = null,
|
||||||
width = null,
|
width = null,
|
||||||
mimetype = null
|
mimeType = null
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.api.media
|
package io.element.android.libraries.matrix.api.media
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
|
|
||||||
interface MatrixMediaLoader {
|
interface MatrixMediaLoader {
|
||||||
/**
|
/**
|
||||||
* @param url to fetch the content for.
|
* @param url to fetch the content for.
|
||||||
|
|
@ -36,7 +34,7 @@ interface MatrixMediaLoader {
|
||||||
/**
|
/**
|
||||||
* @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 [Uri]. It's the uri of the downloaded file.
|
* @return a [Result] of [MediaFile]
|
||||||
*/
|
*/
|
||||||
suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<Uri>
|
suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<MediaFile>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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 java.io.Closeable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around a media file on the disk.
|
||||||
|
* When closed the file will be removed from the disk.
|
||||||
|
*/
|
||||||
|
interface MediaFile : Closeable {
|
||||||
|
fun path(): String
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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.impl.media
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||||
|
import org.matrix.rustcomponents.sdk.MediaFileHandle
|
||||||
|
|
||||||
|
class RustMediaFile(private val inner: MediaFileHandle) : MediaFile {
|
||||||
|
|
||||||
|
override fun path(): String {
|
||||||
|
return inner.path()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
inner.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,15 +16,14 @@
|
||||||
|
|
||||||
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 io.element.android.libraries.matrix.api.media.MatrixMediaSource
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||||
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.io.File
|
|
||||||
|
|
||||||
class RustMediaLoader(
|
class RustMediaLoader(
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
|
@ -59,19 +58,16 @@ class RustMediaLoader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<Uri> =
|
override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<MediaFile> =
|
||||||
withContext(dispatchers.io) {
|
withContext(dispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
mediaSourceFromUrl(source.url).use { mediaSource ->
|
mediaSourceFromUrl(source.url).use { mediaSource ->
|
||||||
innerClient.getMediaFile(
|
val mediaFile = innerClient.getMediaFile(
|
||||||
mediaSource = mediaSource,
|
mediaSource = mediaSource,
|
||||||
mimeType = mimeType ?: "application/octet-stream"
|
mimeType = mimeType ?: "application/octet-stream"
|
||||||
).use {
|
)
|
||||||
val file = File(it.path())
|
RustMediaFile(mediaFile)
|
||||||
Uri.fromFile(file)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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.test.media
|
||||||
|
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||||
|
|
||||||
|
class FakeMediaFile(private val path: String) : MediaFile {
|
||||||
|
override fun path(): String {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() = Unit
|
||||||
|
}
|
||||||
|
|
@ -16,10 +16,9 @@
|
||||||
|
|
||||||
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 io.element.android.libraries.matrix.api.media.MatrixMediaSource
|
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
|
||||||
import java.io.File
|
import io.element.android.libraries.matrix.api.media.MediaFile
|
||||||
|
|
||||||
class FakeMediaLoader : MatrixMediaLoader {
|
class FakeMediaLoader : MatrixMediaLoader {
|
||||||
|
|
||||||
|
|
@ -41,11 +40,11 @@ class FakeMediaLoader : MatrixMediaLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<Uri> {
|
override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<MediaFile> {
|
||||||
return if (shouldFail) {
|
return if (shouldFail) {
|
||||||
Result.failure(RuntimeException())
|
Result.failure(RuntimeException())
|
||||||
} else {
|
} else {
|
||||||
return Result.success(Uri.fromFile(File("path")))
|
return Result.success(FakeMediaFile(""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue