Media: some more improvements over MediaViewer
This commit is contained in:
parent
43f2d74188
commit
a2bd3792f2
16 changed files with 226 additions and 74 deletions
|
|
@ -26,7 +26,6 @@ import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader
|
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
|
@ -41,8 +40,8 @@ import io.element.android.libraries.architecture.BackstackNode
|
||||||
import io.element.android.libraries.architecture.createNode
|
import io.element.android.libraries.architecture.createNode
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.media.MediaSource
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import kotlinx.android.parcel.Parcelize
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@ContributesNode(RoomScope::class)
|
@ContributesNode(RoomScope::class)
|
||||||
class MessagesFlowNode @AssistedInject constructor(
|
class MessagesFlowNode @AssistedInject constructor(
|
||||||
|
|
@ -65,7 +64,8 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
data class MediaViewer(
|
data class MediaViewer(
|
||||||
val title: String,
|
val title: String,
|
||||||
val mediaSource: MediaSource,
|
val mediaSource: MediaSource,
|
||||||
val mimeType: String?
|
val thumbnailSource: MediaSource?,
|
||||||
|
val mimeType: String?,
|
||||||
) : NavTarget
|
) : NavTarget
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|
@ -93,7 +93,12 @@ 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, navTarget.mimeType)
|
val inputs = MediaViewerNode.Inputs(
|
||||||
|
name = navTarget.title,
|
||||||
|
mediaSource = navTarget.mediaSource,
|
||||||
|
thumbnailSource = navTarget.thumbnailSource,
|
||||||
|
mimeType = navTarget.mimeType,
|
||||||
|
)
|
||||||
createNode<MediaViewerNode>(buildContext, listOf(inputs))
|
createNode<MediaViewerNode>(buildContext, listOf(inputs))
|
||||||
}
|
}
|
||||||
is NavTarget.AttachmentPreview -> {
|
is NavTarget.AttachmentPreview -> {
|
||||||
|
|
@ -106,13 +111,22 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
private fun processEventClicked(event: TimelineItem.Event) {
|
private fun processEventClicked(event: TimelineItem.Event) {
|
||||||
when (event.content) {
|
when (event.content) {
|
||||||
is TimelineItemImageContent -> {
|
is TimelineItemImageContent -> {
|
||||||
val mediaSource = event.content.mediaSource
|
val navTarget = NavTarget.MediaViewer(
|
||||||
val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource, event.content.mimeType)
|
title = event.content.body,
|
||||||
|
mediaSource = event.content.mediaSource,
|
||||||
|
thumbnailSource = event.content.mediaSource,
|
||||||
|
mimeType = 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, event.content.mimeType)
|
val navTarget = NavTarget.MediaViewer(
|
||||||
|
title = event.content.body,
|
||||||
|
mediaSource = mediaSource,
|
||||||
|
thumbnailSource = event.content.thumbnailSource,
|
||||||
|
mimeType = event.content.mimeType,
|
||||||
|
)
|
||||||
backstack.push(navTarget)
|
backstack.push(navTarget)
|
||||||
}
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
|
|
@ -124,7 +138,6 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
Children(
|
Children(
|
||||||
navModel = backstack,
|
navModel = backstack,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
transitionHandler = rememberBackstackFader()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,12 @@ 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 androidx.compose.runtime.Immutable
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@Immutable
|
||||||
data class LocalMedia(
|
data class LocalMedia(
|
||||||
val uri: Uri,
|
val uri: Uri,
|
||||||
val mimeType: String,
|
val mimeType: String,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.ui.AspectRatioFrameLayout
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
|
|
@ -47,16 +48,20 @@ import me.saket.telephoto.zoomable.rememberZoomableState
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
@Composable
|
@Composable
|
||||||
fun LocalMediaView(
|
fun LocalMediaView(
|
||||||
localMedia: LocalMedia,
|
localMedia: LocalMedia?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
mimeType: String? = localMedia?.mimeType,
|
||||||
|
onReady: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
MimeTypes.isImage(localMedia.mimeType) -> MediaImageView(
|
MimeTypes.isImage(mimeType) -> MediaImageView(
|
||||||
localMedia = localMedia,
|
localMedia = localMedia,
|
||||||
|
onReady = onReady,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
MimeTypes.isVideo(localMedia.mimeType) -> MediaVideoView(
|
MimeTypes.isVideo(mimeType) -> MediaVideoView(
|
||||||
localMedia = localMedia,
|
localMedia = localMedia,
|
||||||
|
onReady = onReady,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
else -> Unit
|
else -> Unit
|
||||||
|
|
@ -65,7 +70,8 @@ fun LocalMediaView(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MediaImageView(
|
private fun MediaImageView(
|
||||||
localMedia: LocalMedia,
|
localMedia: LocalMedia?,
|
||||||
|
onReady: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (LocalInspectionMode.current) {
|
if (LocalInspectionMode.current) {
|
||||||
|
|
@ -78,10 +84,16 @@ private fun MediaImageView(
|
||||||
val zoomableState = rememberZoomableState(
|
val zoomableState = rememberZoomableState(
|
||||||
zoomSpec = ZoomSpec(maxZoomFactor = 3f)
|
zoomSpec = ZoomSpec(maxZoomFactor = 3f)
|
||||||
)
|
)
|
||||||
|
val zoomableImageState = rememberZoomableImageState(zoomableState)
|
||||||
|
LaunchedEffect(zoomableImageState.isImageDisplayed) {
|
||||||
|
if (zoomableImageState.isImageDisplayed) {
|
||||||
|
onReady()
|
||||||
|
}
|
||||||
|
}
|
||||||
ZoomableAsyncImage(
|
ZoomableAsyncImage(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
state = rememberZoomableImageState(zoomableState),
|
state = zoomableImageState,
|
||||||
model = localMedia.model,
|
model = localMedia?.model,
|
||||||
contentDescription = "Image",
|
contentDescription = "Image",
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
)
|
)
|
||||||
|
|
@ -91,22 +103,32 @@ private fun MediaImageView(
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@Composable
|
@Composable
|
||||||
fun MediaVideoView(
|
fun MediaVideoView(
|
||||||
localMedia: LocalMedia,
|
localMedia: LocalMedia?,
|
||||||
|
onReady: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val playerListener = object : Player.Listener {
|
||||||
|
override fun onRenderedFirstFrame() {
|
||||||
|
onReady()
|
||||||
|
}
|
||||||
|
}
|
||||||
val exoPlayer = remember {
|
val exoPlayer = remember {
|
||||||
ExoPlayer.Builder(context).build()
|
ExoPlayer.Builder(context)
|
||||||
|
.build()
|
||||||
.apply {
|
.apply {
|
||||||
this.playWhenReady = true
|
addListener(playerListener)
|
||||||
this.prepare()
|
this.prepare()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LaunchedEffect(localMedia.uri) {
|
if (localMedia?.uri != null) {
|
||||||
val mediaItem = MediaItem.fromUri(localMedia.uri)
|
LaunchedEffect(localMedia.uri) {
|
||||||
exoPlayer.setMediaItem(mediaItem)
|
val mediaItem = MediaItem.fromUri(localMedia.uri)
|
||||||
|
exoPlayer.setMediaItem(mediaItem)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exoPlayer.setMediaItems(emptyList())
|
||||||
}
|
}
|
||||||
|
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = {
|
factory = {
|
||||||
PlayerView(context).apply {
|
PlayerView(context).apply {
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,5 @@ package io.element.android.features.messages.impl.media.viewer
|
||||||
|
|
||||||
sealed interface MediaViewerEvents {
|
sealed interface MediaViewerEvents {
|
||||||
object RetryLoading : MediaViewerEvents
|
object RetryLoading : MediaViewerEvents
|
||||||
|
object ClearLoadingError : MediaViewerEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ class MediaViewerNode @AssistedInject constructor(
|
||||||
data class Inputs(
|
data class Inputs(
|
||||||
val name: String,
|
val name: String,
|
||||||
val mediaSource: MediaSource,
|
val mediaSource: MediaSource,
|
||||||
|
val thumbnailSource: MediaSource?,
|
||||||
val mimeType: String?
|
val mimeType: String?
|
||||||
) : NodeInputs
|
) : NodeInputs
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,14 @@ class MediaViewerPresenter @AssistedInject constructor(
|
||||||
fun handleEvents(mediaViewerEvents: MediaViewerEvents) {
|
fun handleEvents(mediaViewerEvents: MediaViewerEvents) {
|
||||||
when (mediaViewerEvents) {
|
when (mediaViewerEvents) {
|
||||||
MediaViewerEvents.RetryLoading -> loadMediaTrigger++
|
MediaViewerEvents.RetryLoading -> loadMediaTrigger++
|
||||||
|
MediaViewerEvents.ClearLoadingError -> localMedia.value = Async.Uninitialized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return MediaViewerState(
|
return MediaViewerState(
|
||||||
name = inputs.name,
|
name = inputs.name,
|
||||||
|
mimeType = inputs.mimeType,
|
||||||
|
thumbnailSource = inputs.thumbnailSource,
|
||||||
downloadedMedia = localMedia.value,
|
downloadedMedia = localMedia.value,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,12 @@ package io.element.android.features.messages.impl.media.viewer
|
||||||
|
|
||||||
import io.element.android.features.messages.impl.media.local.LocalMedia
|
import io.element.android.features.messages.impl.media.local.LocalMedia
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
|
|
||||||
data class MediaViewerState(
|
data class MediaViewerState(
|
||||||
val name: String,
|
val name: String,
|
||||||
|
val mimeType: String?,
|
||||||
|
val thumbnailSource: MediaSource?,
|
||||||
val downloadedMedia: Async<LocalMedia>,
|
val downloadedMedia: Async<LocalMedia>,
|
||||||
val eventSink: (MediaViewerEvents) -> Unit
|
val eventSink: (MediaViewerEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,14 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
|
||||||
aMediaViewerState(
|
aMediaViewerState(
|
||||||
Async.Success(
|
Async.Success(
|
||||||
LocalMedia(
|
LocalMedia(
|
||||||
Uri.EMPTY, MimeTypes.IMAGE_JPEG, "a file", 100L
|
Uri.EMPTY, MimeTypes.IMAGE_JPEG, "an image file", 100L
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
aMediaViewerState(
|
||||||
|
Async.Success(
|
||||||
|
LocalMedia(
|
||||||
|
Uri.EMPTY, MimeTypes.VIDEO_MP4, "a video file", 100L
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -40,6 +47,7 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
|
||||||
|
|
||||||
fun aMediaViewerState(downloadedMedia: Async<LocalMedia> = Async.Uninitialized) = MediaViewerState(
|
fun aMediaViewerState(downloadedMedia: Async<LocalMedia> = Async.Uninitialized) = MediaViewerState(
|
||||||
name = "A media",
|
name = "A media",
|
||||||
|
mimeType = MimeTypes.IMAGE_JPEG,
|
||||||
|
thumbnailSource = null,
|
||||||
downloadedMedia = downloadedMedia,
|
downloadedMedia = downloadedMedia,
|
||||||
eventSink = {}
|
) {}
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -14,33 +14,36 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
|
|
||||||
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.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
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.res.stringResource
|
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 coil.compose.AsyncImage
|
||||||
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.architecture.isLoading
|
||||||
|
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
|
||||||
|
import io.element.android.libraries.designsystem.modifiers.roundedBackground
|
||||||
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.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.matrix.api.media.MediaSource
|
||||||
|
import io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import io.element.android.libraries.ui.strings.R as StringR
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -53,6 +56,32 @@ fun MediaViewerView(
|
||||||
state.eventSink(MediaViewerEvents.RetryLoading)
|
state.eventSink(MediaViewerEvents.RetryLoading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onDismissError() {
|
||||||
|
state.eventSink(MediaViewerEvents.ClearLoadingError)
|
||||||
|
}
|
||||||
|
|
||||||
|
var showProgress by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trick to avoid showing progress indicator if the media is already on disk.
|
||||||
|
// When sdk will expose download progress we'll be able to remove this.
|
||||||
|
LaunchedEffect(state.downloadedMedia) {
|
||||||
|
showProgress = false
|
||||||
|
delay(100)
|
||||||
|
if (state.downloadedMedia.isLoading()) {
|
||||||
|
showProgress = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var showThumbnail by remember {
|
||||||
|
mutableStateOf(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMediaReady() {
|
||||||
|
showThumbnail = false
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(modifier) {
|
Scaffold(modifier) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -60,10 +89,55 @@ fun MediaViewerView(
|
||||||
.padding(it),
|
.padding(it),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
when (state.downloadedMedia) {
|
if (state.downloadedMedia is Async.Failure) {
|
||||||
is Async.Success -> LocalMediaView(state.downloadedMedia.state)
|
ErrorView(
|
||||||
is Async.Failure -> ErrorView(stringResource(id = StringR.string.error_unknown), ::onRetry)
|
errorMessage = stringResource(id = StringR.string.error_unknown),
|
||||||
else -> CircularProgressIndicator()
|
onRetry = ::onRetry,
|
||||||
|
onDismiss = ::onDismissError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LocalMediaView(
|
||||||
|
localMedia = state.downloadedMedia.dataOrNull(),
|
||||||
|
mimeType = state.mimeType,
|
||||||
|
onReady = ::onMediaReady
|
||||||
|
)
|
||||||
|
ThumbnailView(
|
||||||
|
thumbnailSource = state.thumbnailSource,
|
||||||
|
showThumbnail = showThumbnail,
|
||||||
|
showProgress = showProgress,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ThumbnailView(
|
||||||
|
thumbnailSource: MediaSource?,
|
||||||
|
showThumbnail: Boolean,
|
||||||
|
showProgress: Boolean,
|
||||||
|
) {
|
||||||
|
if (!showThumbnail) return
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
val mediaRequestData = MediaRequestData(
|
||||||
|
source = thumbnailSource,
|
||||||
|
kind = MediaRequestData.Kind.Content
|
||||||
|
)
|
||||||
|
AsyncImage(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
model = mediaRequestData,
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
if (showProgress) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.roundedBackground(),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,21 +147,15 @@ fun MediaViewerView(
|
||||||
private fun ErrorView(
|
private fun ErrorView(
|
||||||
errorMessage: String,
|
errorMessage: String,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
RetryDialog(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
content = errorMessage,
|
||||||
) {
|
onRetry = onRetry,
|
||||||
Text(text = errorMessage)
|
onDismiss = onDismiss
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
)
|
||||||
Button(
|
|
||||||
onClick = onRetry
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(id = StringR.string.action_retry))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
|
|
||||||
|
|
@ -17,26 +17,22 @@
|
||||||
package io.element.android.features.messages.impl.timeline.components.event
|
package io.element.android.features.messages.impl.timeline.components.event
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
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.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
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.components.blurhash.BlurHashAsyncImage
|
import io.element.android.features.messages.impl.timeline.components.blurhash.BlurHashAsyncImage
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider
|
||||||
|
import io.element.android.libraries.designsystem.modifiers.roundedBackground
|
||||||
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 io.element.android.libraries.matrix.ui.media.MediaRequestData
|
||||||
|
|
@ -54,15 +50,12 @@ fun TimelineItemVideoView(
|
||||||
) {
|
) {
|
||||||
BlurHashAsyncImage(
|
BlurHashAsyncImage(
|
||||||
model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.Content),
|
model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.Content),
|
||||||
blurHash = content.blurhash,
|
blurHash = content.blurHash,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.roundedBackground(),
|
||||||
.size(50.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(color = Color.Black.copy(alpha = 0.5f)),
|
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,11 @@ class TimelineItemContentMessageFactory @Inject constructor() {
|
||||||
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height)
|
||||||
TimelineItemImageContent(
|
TimelineItemImageContent(
|
||||||
body = messageType.body,
|
body = messageType.body,
|
||||||
height = messageType.info?.height?.toInt(),
|
|
||||||
width = messageType.info?.width?.toInt(),
|
|
||||||
mimeType = messageType.info?.mimetype,
|
|
||||||
mediaSource = messageType.source,
|
mediaSource = messageType.source,
|
||||||
|
mimeType = messageType.info?.mimetype,
|
||||||
blurhash = messageType.info?.blurhash,
|
blurhash = messageType.info?.blurhash,
|
||||||
|
width = messageType.info?.width?.toInt(),
|
||||||
|
height = messageType.info?.height?.toInt(),
|
||||||
aspectRatio = aspectRatio
|
aspectRatio = aspectRatio
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +64,7 @@ class TimelineItemContentMessageFactory @Inject constructor() {
|
||||||
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,
|
||||||
blurhash = messageType.info?.blurhash,
|
blurHash = messageType.info?.blurhash,
|
||||||
aspectRatio = aspectRatio
|
aspectRatio = aspectRatio
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,9 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider<TimelineI
|
||||||
fun aTimelineItemImageContent() = TimelineItemImageContent(
|
fun aTimelineItemImageContent() = TimelineItemImageContent(
|
||||||
body = "a body",
|
body = "a body",
|
||||||
mediaSource = MediaSource(""),
|
mediaSource = MediaSource(""),
|
||||||
blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
|
|
||||||
aspectRatio = 0.5f,
|
|
||||||
mimeType = MimeTypes.IMAGE_JPEG,
|
mimeType = MimeTypes.IMAGE_JPEG,
|
||||||
|
blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
|
||||||
|
width = null,
|
||||||
height = 300,
|
height = 300,
|
||||||
width = null
|
aspectRatio = 0.5f
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ data class TimelineItemVideoContent(
|
||||||
val videoSource: MediaSource,
|
val videoSource: MediaSource,
|
||||||
val thumbnailSource: MediaSource?,
|
val thumbnailSource: MediaSource?,
|
||||||
val aspectRatio: Float,
|
val aspectRatio: Float,
|
||||||
val blurhash: String?,
|
val blurHash: String?,
|
||||||
val height: Int?,
|
val height: Int?,
|
||||||
val width: Int?,
|
val width: Int?,
|
||||||
val mimeType: String?,
|
val mimeType: String?,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ open class TimelineItemVideoContentProvider : PreviewParameterProvider<TimelineI
|
||||||
fun aTimelineItemVideoContent() = TimelineItemVideoContent(
|
fun aTimelineItemVideoContent() = TimelineItemVideoContent(
|
||||||
body = "a video",
|
body = "a video",
|
||||||
thumbnailSource = MediaSource(url = ""),
|
thumbnailSource = MediaSource(url = ""),
|
||||||
blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
|
blurHash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
|
||||||
aspectRatio = 0.5f,
|
aspectRatio = 0.5f,
|
||||||
duration = 100,
|
duration = 100,
|
||||||
videoSource = MediaSource(""),
|
videoSource = MediaSource(""),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* 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.designsystem.modifiers
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This modifier can be use to provide a nice background for Icon or ProgressIndicator.
|
||||||
|
*/
|
||||||
|
fun Modifier.roundedBackground(
|
||||||
|
size: Dp = 48.dp,
|
||||||
|
color: Color = Color.Black,
|
||||||
|
alpha: Float = 0.5f,
|
||||||
|
) = this
|
||||||
|
.size(size)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(color = color.copy(alpha = alpha))
|
||||||
|
.padding(8.dp)
|
||||||
|
|
@ -23,8 +23,6 @@ import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.SpaceId
|
import io.element.android.libraries.matrix.api.core.SpaceId
|
||||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import kotlin.time.DurationUnit
|
|
||||||
import kotlin.time.toDuration
|
|
||||||
|
|
||||||
const val A_USER_NAME = "alice"
|
const val A_USER_NAME = "alice"
|
||||||
const val A_PASSWORD = "password"
|
const val A_PASSWORD = "password"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue