Browse Source

Media: address PR review

feature/fga/small_timeline_improvements
ganfra 1 year ago
parent
commit
a09ea589f2
  1. 1
      build.gradle.kts
  2. 12
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/helper/fileExtensionAndSize.kt
  3. 67
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt
  4. 21
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt
  5. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt
  6. 53
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt
  7. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt

1
build.gradle.kts

@ -224,6 +224,7 @@ koverMerged {
excludes += "io.element.android.libraries.matrix.api.room.MatrixRoomMembersState*" excludes += "io.element.android.libraries.matrix.api.room.MatrixRoomMembersState*"
excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*" excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*"
excludes += "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState" excludes += "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState"
excludes += "io.element.android.features.messages.impl.media.local.LocalMediaViewState"
} }
bound { bound {
minValue = 90 minValue = 90

12
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/helper/fileExtensionAndSize.kt

@ -16,10 +16,18 @@
package io.element.android.features.messages.impl.media.helper package io.element.android.features.messages.impl.media.helper
import android.webkit.MimeTypeMap
fun formatFileExtensionAndSize(name: String, size: String?): String { fun formatFileExtensionAndSize(name: String, size: String?): String {
val fileExtension = name.substringAfterLast('.', "").uppercase() val fileExtension = name.substringAfterLast('.', "")
// Makes sure the extension is known by the system, otherwise default to binary extension.
val safeExtension = if (MimeTypeMap.getSingleton().hasExtension(fileExtension)) {
fileExtension.uppercase()
} else {
"BIN"
}
return buildString { return buildString {
append(fileExtension) append(safeExtension)
if (size != null) { if (size != null) {
append(' ') append(' ')
append("($size)") append("($size)")

67
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt

@ -34,7 +34,11 @@ import androidx.compose.material.icons.outlined.Attachment
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.draw.clip import androidx.compose.ui.draw.clip
@ -70,40 +74,52 @@ import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
import me.saket.telephoto.zoomable.rememberZoomableImageState import me.saket.telephoto.zoomable.rememberZoomableImageState
import me.saket.telephoto.zoomable.rememberZoomableState import me.saket.telephoto.zoomable.rememberZoomableState
@Stable
class LocalMediaViewState {
var isReady: Boolean by mutableStateOf(false)
}
@Composable
fun rememberLocalMediaViewState(): LocalMediaViewState {
return remember {
LocalMediaViewState()
}
}
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
@Composable @Composable
fun LocalMediaView( fun LocalMediaView(
localMedia: LocalMedia?, localMedia: LocalMedia?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
info: MediaInfo? = localMedia?.info, localMediaViewState: LocalMediaViewState = rememberLocalMediaViewState(),
onReady: () -> Unit = {}, mediaInfo: MediaInfo? = localMedia?.info,
) { ) {
val zoomableState = rememberZoomableState( val zoomableState = rememberZoomableState(
zoomSpec = ZoomSpec(maxZoomFactor = 5f) zoomSpec = ZoomSpec(maxZoomFactor = 5f)
) )
val mimeType = info?.mimeType val mimeType = mediaInfo?.mimeType
when { when {
mimeType.isMimeTypeImage() -> MediaImageView( mimeType.isMimeTypeImage() -> MediaImageView(
localMediaViewState = localMediaViewState,
localMedia = localMedia, localMedia = localMedia,
zoomableState = zoomableState, zoomableState = zoomableState,
onReady = onReady,
modifier = modifier modifier = modifier
) )
mimeType.isMimeTypeVideo() -> MediaVideoView( mimeType.isMimeTypeVideo() -> MediaVideoView(
localMediaViewState = localMediaViewState,
localMedia = localMedia, localMedia = localMedia,
onReady = onReady,
modifier = modifier modifier = modifier
) )
mimeType == MimeTypes.Pdf -> MediaPDFView( mimeType == MimeTypes.Pdf -> MediaPDFView(
localMediaViewState = localMediaViewState,
localMedia = localMedia, localMedia = localMedia,
zoomableState = zoomableState, zoomableState = zoomableState,
onReady = onReady,
modifier = modifier modifier = modifier
) )
else -> MediaFileView( else -> MediaFileView(
localMediaViewState = localMediaViewState,
uri = localMedia?.uri, uri = localMedia?.uri,
info = info, info = mediaInfo,
onReady = onReady,
modifier = modifier modifier = modifier
) )
} }
@ -111,9 +127,9 @@ fun LocalMediaView(
@Composable @Composable
private fun MediaImageView( private fun MediaImageView(
localMediaViewState: LocalMediaViewState,
localMedia: LocalMedia?, localMedia: LocalMedia?,
zoomableState: ZoomableState, zoomableState: ZoomableState,
onReady: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (LocalInspectionMode.current) { if (LocalInspectionMode.current) {
@ -124,11 +140,7 @@ private fun MediaImageView(
) )
} else { } else {
val zoomableImageState = rememberZoomableImageState(zoomableState) val zoomableImageState = rememberZoomableImageState(zoomableState)
LaunchedEffect(zoomableImageState.isImageDisplayed) { localMediaViewState.isReady = zoomableImageState.isImageDisplayed
if (zoomableImageState.isImageDisplayed) {
onReady()
}
}
ZoomableAsyncImage( ZoomableAsyncImage(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
state = zoomableImageState, state = zoomableImageState,
@ -142,14 +154,14 @@ private fun MediaImageView(
@UnstableApi @UnstableApi
@Composable @Composable
fun MediaVideoView( fun MediaVideoView(
localMediaViewState: LocalMediaViewState,
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 { val playerListener = object : Player.Listener {
override fun onRenderedFirstFrame() { override fun onRenderedFirstFrame() {
onReady() localMediaViewState.isReady = true
} }
} }
val exoPlayer = remember { val exoPlayer = remember {
@ -196,35 +208,27 @@ fun MediaVideoView(
@Composable @Composable
fun MediaPDFView( fun MediaPDFView(
localMediaViewState: LocalMediaViewState,
localMedia: LocalMedia?, localMedia: LocalMedia?,
zoomableState: ZoomableState, zoomableState: ZoomableState,
onReady: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val pdfViewerState = rememberPdfViewerState( val pdfViewerState = rememberPdfViewerState(
model = localMedia?.uri, model = localMedia?.uri,
zoomableState = zoomableState zoomableState = zoomableState
) )
LaunchedEffect(pdfViewerState.isLoaded) { localMediaViewState.isReady = pdfViewerState.isLoaded
if (pdfViewerState.isLoaded) {
onReady()
}
}
PdfViewer(pdfViewerState = pdfViewerState, modifier = modifier) PdfViewer(pdfViewerState = pdfViewerState, modifier = modifier)
} }
@Composable @Composable
fun MediaFileView( fun MediaFileView(
localMediaViewState: LocalMediaViewState,
uri: Uri?, uri: Uri?,
info: MediaInfo?, info: MediaInfo?,
onReady: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LaunchedEffect(Unit) { localMediaViewState.isReady = uri != null
if(uri != null) {
onReady()
}
}
Box(modifier = modifier, contentAlignment = Alignment.Center) { Box(modifier = modifier, contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box( Box(
@ -236,14 +240,14 @@ fun MediaFileView(
) { ) {
Icon( Icon(
imageVector = Icons.Outlined.Attachment, imageVector = Icons.Outlined.Attachment,
contentDescription = "OpenFile", contentDescription = null,
tint = MaterialTheme.colorScheme.background, tint = MaterialTheme.colorScheme.background,
modifier = Modifier modifier = Modifier
.size(32.dp) .size(32.dp)
.rotate(-45f), .rotate(-45f),
) )
} }
if(info == null) return if (info != null) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = info.name, text = info.name,
@ -255,7 +259,10 @@ fun MediaFileView(
Text( Text(
text = formatFileExtensionAndSize(info.name, info.formattedFileSize), text = formatFileExtensionAndSize(info.name, info.formattedFileSize),
fontSize = 14.sp, fontSize = 14.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
} }
} }
} }
}

21
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt

@ -116,8 +116,7 @@ class MediaViewerPresenter @AssistedInject constructor(
} }
private fun CoroutineScope.saveOnDisk(localMedia: Async<LocalMedia>) = launch { private fun CoroutineScope.saveOnDisk(localMedia: Async<LocalMedia>) = launch {
when (localMedia) { if (localMedia is Async.Success) {
is Async.Success -> {
localMediaActions.saveOnDisk(localMedia.state) localMediaActions.saveOnDisk(localMedia.state)
.onSuccess { .onSuccess {
val snackbarMessage = SnackbarMessage(StringR.string.common_file_saved_on_disk_android) val snackbarMessage = SnackbarMessage(StringR.string.common_file_saved_on_disk_android)
@ -127,35 +126,27 @@ class MediaViewerPresenter @AssistedInject constructor(
val snackbarMessage = SnackbarMessage(mediaActionsError(it)) val snackbarMessage = SnackbarMessage(mediaActionsError(it))
snackbarDispatcher.post(snackbarMessage) snackbarDispatcher.post(snackbarMessage)
} }
} } else Unit
else -> Unit
}
} }
private fun CoroutineScope.share(localMedia: Async<LocalMedia>) = launch { private fun CoroutineScope.share(localMedia: Async<LocalMedia>) = launch {
when (localMedia) { if (localMedia is Async.Success) {
is Async.Success -> {
localMediaActions.share(localMedia.state) localMediaActions.share(localMedia.state)
.onFailure { .onFailure {
val snackbarMessage = SnackbarMessage(mediaActionsError(it)) val snackbarMessage = SnackbarMessage(mediaActionsError(it))
snackbarDispatcher.post(snackbarMessage) snackbarDispatcher.post(snackbarMessage)
} }
} } else Unit
else -> Unit
}
} }
private fun CoroutineScope.open(localMedia: Async<LocalMedia>) = launch { private fun CoroutineScope.open(localMedia: Async<LocalMedia>) = launch {
when (localMedia) { if (localMedia is Async.Success) {
is Async.Success -> {
localMediaActions.open(localMedia.state) localMediaActions.open(localMedia.state)
.onFailure { .onFailure {
val snackbarMessage = SnackbarMessage(mediaActionsError(it)) val snackbarMessage = SnackbarMessage(mediaActionsError(it))
snackbarDispatcher.post(snackbarMessage) snackbarDispatcher.post(snackbarMessage)
} }
} } else Unit
else -> Unit
}
} }
private fun mediaActionsError(throwable: Throwable): Int { private fun mediaActionsError(throwable: Throwable): Int {

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt

@ -50,6 +50,10 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
), ),
aPdfInfo(), aPdfInfo(),
), ),
aMediaViewerState(
Async.Loading(),
aFileInfo(),
),
aMediaViewerState( aMediaViewerState(
Async.Success( Async.Success(
LocalMedia(Uri.EMPTY, aFileInfo()) LocalMedia(Uri.EMPTY, aFileInfo())

53
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt

@ -46,12 +46,15 @@ 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.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode
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 androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import io.element.android.features.messages.impl.media.local.LocalMedia
import io.element.android.features.messages.impl.media.local.LocalMediaView import io.element.android.features.messages.impl.media.local.LocalMediaView
import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState
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.architecture.isLoading
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
@ -82,28 +85,9 @@ fun MediaViewerView(
state.eventSink(MediaViewerEvents.ClearLoadingError) state.eventSink(MediaViewerEvents.ClearLoadingError)
} }
var showProgress by remember { val localMediaViewState = rememberLocalMediaViewState()
mutableStateOf(false) val showThumbnail = !localMediaViewState.isReady
} val showProgress = rememberShowProgress(state.downloadedMedia)
// 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
}
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
Scaffold( Scaffold(
@ -151,9 +135,9 @@ fun MediaViewerView(
) )
} }
LocalMediaView( LocalMediaView(
localMediaViewState = localMediaViewState,
localMedia = state.downloadedMedia.dataOrNull(), localMedia = state.downloadedMedia.dataOrNull(),
info = state.mediaInfo, mediaInfo = state.mediaInfo,
onReady = ::onMediaReady
) )
ThumbnailView( ThumbnailView(
thumbnailSource = state.thumbnailSource, thumbnailSource = state.thumbnailSource,
@ -164,6 +148,27 @@ fun MediaViewerView(
} }
} }
@Composable
private fun rememberShowProgress(downloadedMedia: Async<LocalMedia>): Boolean {
var showProgress by remember {
mutableStateOf(false)
}
if (LocalInspectionMode.current) {
showProgress = downloadedMedia.isLoading()
} else {
// 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(downloadedMedia) {
showProgress = false
delay(100)
if (downloadedMedia.isLoading()) {
showProgress = true
}
}
}
return showProgress
}
@Composable @Composable
private fun MediaViewerTopBar( private fun MediaViewerTopBar(
actionsEnabled: Boolean, actionsEnabled: Boolean,

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt

@ -79,6 +79,8 @@ fun TimelineItemFileView(
text = content.fileExtensionAndSize, text = content.fileExtensionAndSize,
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
fontSize = 12.sp, fontSize = 12.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
} }
} }

Loading…
Cancel
Save