Media: some more improvements over MediaViewer

This commit is contained in:
ganfra 2023-05-25 22:54:09 +02:00
parent 43f2d74188
commit a2bd3792f2
16 changed files with 226 additions and 74 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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