From 9dcc3780ac006cf832e4c04a074284146aa9fc1c Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 26 Apr 2023 16:12:52 +0200 Subject: [PATCH 01/48] Timeline: Remove padding from parent Modifier --- .../messages/impl/timeline/TimelineView.kt | 20 +++++++++++-------- .../event/TimelineItemContentView.kt | 16 ++++++++++----- .../components/event/TimelineItemImageView.kt | 9 +++++++-- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 7ea427a8cf..f252494df6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn @@ -212,12 +211,13 @@ fun TimelineItemEventRow( Modifier.zIndex(1f) ) } + val bubbleState = BubbleState( + groupPosition = event.groupPosition, + isMine = event.isMine, + isHighlighted = isHighlighted, + ) MessageEventBubble( - state = BubbleState( - groupPosition = event.groupPosition, - isMine = event.isMine, - isHighlighted = isHighlighted, - ), + state = bubbleState, interactionSource = interactionSource, onClick = onClick, onLongClick = onLongClick, @@ -225,8 +225,12 @@ fun TimelineItemEventRow( .zIndex(-1f) .widthIn(max = 320.dp) ) { - val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) - TimelineItemEventContentView(event.content, interactionSource, onClick, onLongClick, contentModifier) + TimelineItemEventContentView( + content = event.content, + interactionSource = interactionSource, + onClick = onClick, + onLongClick = onLongClick, + ) } TimelineItemReactionsView( reactionsState = event.reactionsState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt index bfb1d7f0c7..5171c92c40 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt @@ -17,8 +17,10 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent @@ -26,6 +28,10 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent +private fun Modifier.defaultContentPadding(): Modifier = padding( + horizontal = 12.dp, vertical = 6.dp +) + @Composable fun TimelineItemEventContentView( content: TimelineItemEventContent, @@ -37,26 +43,26 @@ fun TimelineItemEventContentView( when (content) { is TimelineItemEncryptedContent -> TimelineItemEncryptedView( content = content, - modifier = modifier + modifier = modifier.defaultContentPadding() ) is TimelineItemRedactedContent -> TimelineItemRedactedView( content = content, - modifier = modifier + modifier = modifier.defaultContentPadding() ) is TimelineItemTextBasedContent -> TimelineItemTextView( content = content, interactionSource = interactionSource, - modifier = modifier, + modifier = modifier.defaultContentPadding(), onTextClicked = onClick, onTextLongClicked = onLongClick ) is TimelineItemUnknownContent -> TimelineItemUnknownView( content = content, - modifier = modifier + modifier = modifier.defaultContentPadding() ) is TimelineItemImageContent -> TimelineItemImageView( content = content, - modifier = modifier + modifier = modifier, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index a532557ffb..ffc30e012f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -37,11 +37,12 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground +import timber.log.Timber @Composable fun TimelineItemImageView( content: TimelineItemImageContent, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val widthPercent = if (content.aspectRatio > 1f) { 1f @@ -65,7 +66,11 @@ fun TimelineItemImageView( contentDescription = null, placeholder = debugPlaceholderBackground(ColorPainter(MaterialTheme.colorScheme.surfaceVariant)), contentScale = ContentScale.Crop, - onSuccess = { isLoading.value = false }, + onSuccess = { + Timber.v("OnSuccess = ${it.result.dataSource}") + isLoading.value = false + }, + onError = { Timber.e(it.result.throwable) } ) } } From c920dfb97adeed486f2d5e5b4f43b899cf59b60f Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 26 Apr 2023 16:13:09 +0200 Subject: [PATCH 02/48] MediaResolver: exposes Result instead of Nullable --- .../libraries/matrix/api/media/MediaResolver.kt | 2 +- .../matrix/impl/media/RustMediaResolver.kt | 7 ++++--- .../matrix/test/media/FakeMediaResolver.kt | 11 +++++++++-- .../libraries/matrix/ui/media/MediaFetcher.kt | 15 +++++++++++---- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaResolver.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaResolver.kt index 52d674df90..058c510ffd 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaResolver.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaResolver.kt @@ -31,6 +31,6 @@ interface MediaResolver { val kind: Kind ) - suspend fun resolve(url: String?, kind: Kind): ByteArray? + suspend fun resolve(url: String?, kind: Kind): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaResolver.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaResolver.kt index 68ed48350e..a708dc0d31 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaResolver.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaResolver.kt @@ -18,11 +18,12 @@ package io.element.android.libraries.matrix.impl.media import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.media.MediaResolver +import java.lang.IllegalStateException internal class RustMediaResolver(private val client: MatrixClient) : MediaResolver { - override suspend fun resolve(url: String?, kind: MediaResolver.Kind): ByteArray? { - if (url.isNullOrEmpty()) return null + override suspend fun resolve(url: String?, kind: MediaResolver.Kind): Result { + if (url.isNullOrEmpty()) return Result.failure(IllegalStateException("The url is null or empty")) return when (kind) { is MediaResolver.Kind.Content -> client.loadMediaContent(url) is MediaResolver.Kind.Thumbnail -> client.loadMediaThumbnail( @@ -30,6 +31,6 @@ internal class RustMediaResolver(private val client: MatrixClient) : MediaResolv kind.width.toLong(), kind.height.toLong() ) - }.getOrNull() + } } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaResolver.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaResolver.kt index 4d5ebc8029..c1b6e52003 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaResolver.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaResolver.kt @@ -19,7 +19,14 @@ package io.element.android.libraries.matrix.test.media import io.element.android.libraries.matrix.api.media.MediaResolver class FakeMediaResolver : MediaResolver { - override suspend fun resolve(url: String?, kind: MediaResolver.Kind): ByteArray? { - return null + + private var result: Result = Result.success(ByteArray(0)) + + fun givenResult(result: Result) { + this.result = result + } + + override suspend fun resolve(url: String?, kind: MediaResolver.Kind): Result { + return result } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaFetcher.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaFetcher.kt index 6567101162..4e1821e8cf 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaFetcher.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaFetcher.kt @@ -26,16 +26,23 @@ import io.element.android.libraries.matrix.api.media.MediaResolver import java.nio.ByteBuffer internal class MediaFetcher( - private val mediaResolver: MediaResolver?, + private val mediaResolver: MediaResolver, private val meta: MediaResolver.Meta, private val options: Options, private val imageLoader: ImageLoader ) : Fetcher { override suspend fun fetch(): FetchResult? { - val byteArray = mediaResolver?.resolve(meta.url, meta.kind) ?: return null - val byteBuffer = ByteBuffer.wrap(byteArray) - return imageLoader.components.newFetcher(byteBuffer, options, imageLoader)?.first?.fetch() + return mediaResolver.resolve(meta.url, meta.kind) + .map { byteArray -> + ByteBuffer.wrap(byteArray) + }.map { byteBuffer -> + imageLoader.components.newFetcher(byteBuffer, options, imageLoader)?.first?.fetch() + } + .fold( + { result -> result }, + { failure -> throw failure } + ) } class MetaFactory(private val client: MatrixClient) : From 4b5ca3acdd60bd5eb40b4e2c9506cc11a9ccdd2a Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 27 Apr 2023 12:06:01 +0200 Subject: [PATCH 03/48] Media: change the API --- .../components/event/TimelineItemImageView.kt | 4 +- .../TimelineItemContentMessageFactory.kt | 6 +- .../model/event/TimelineItemImageContent.kt | 4 +- .../event/TimelineItemImageContentProvider.kt | 4 +- .../libraries/matrix/api/MatrixClient.kt | 10 +-- .../matrix/api/media/MatrixMediaLoader.kt | 42 +++++++++++ .../matrix/api/media/MediaResolver.kt | 36 --------- .../libraries/matrix/impl/RustMatrixClient.kt | 40 ++-------- .../matrix/impl/media/RustMediaLoader.kt | 75 +++++++++++++++++++ .../matrix/impl/media/RustMediaResolver.kt | 36 --------- .../libraries/matrix/test/FakeMatrixClient.kt | 17 +---- .../matrix/test/media/FakeMediaLoader.kt | 50 +++++++++++++ .../matrix/test/media/FakeMediaResolver.kt | 32 -------- .../{AvatarDataExt.kt => AvatatarDataExt.kt} | 12 ++- .../{MediaFetcher.kt => CoilMediaFetcher.kt} | 45 +++++++---- .../matrix/ui/media/ImageLoaderFactories.kt | 8 +- .../matrix/ui/media/MediaRequestData.kt} | 22 +++--- ...MediaKeyer.kt => MediaRequestDataKeyer.kt} | 11 ++- 18 files changed, 242 insertions(+), 212 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt delete mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaResolver.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt delete mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaResolver.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt delete mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaResolver.kt rename libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/{AvatarDataExt.kt => AvatatarDataExt.kt} (74%) rename libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/{MediaFetcher.kt => CoilMediaFetcher.kt} (60%) rename libraries/{matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaResolver.kt => matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt} (62%) rename libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/{MediaKeyer.kt => MediaRequestDataKeyer.kt} (72%) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index ffc30e012f..ae2d18f541 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -55,10 +55,10 @@ fun TimelineItemImageView( .aspectRatio(content.aspectRatio), contentAlignment = Alignment.Center, ) { - val isLoading = rememberSaveable(content.imageMeta) { mutableStateOf(true) } + val isLoading = rememberSaveable(content.mediaRequestData) { mutableStateOf(true) } val context = LocalContext.current val model = ImageRequest.Builder(context) - .data(content.imageMeta) + .data(content.mediaRequestData) .build() AsyncImage( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index c0b4a4f5cc..57d6489592 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -23,12 +23,12 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.util.toHtmlDocument -import io.element.android.libraries.matrix.api.media.MediaResolver import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.matrix.ui.media.MediaRequestData import javax.inject.Inject class TimelineItemContentMessageFactory @Inject constructor() { @@ -49,9 +49,9 @@ class TimelineItemContentMessageFactory @Inject constructor() { } TimelineItemImageContent( body = messageType.body, - imageMeta = MediaResolver.Meta( + mediaRequestData = MediaRequestData( url = messageType.url, - kind = MediaResolver.Kind.Content + kind = MediaRequestData.Kind.Content ), blurhash = messageType.info?.blurhash, aspectRatio = aspectRatio diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt index 8013ca8a65..3f00a3c183 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt @@ -16,11 +16,11 @@ package io.element.android.features.messages.impl.timeline.model.event -import io.element.android.libraries.matrix.api.media.MediaResolver +import io.element.android.libraries.matrix.ui.media.MediaRequestData data class TimelineItemImageContent( val body: String, - val imageMeta: MediaResolver.Meta, + val mediaRequestData: MediaRequestData, val blurhash: String?, val aspectRatio: Float ) : TimelineItemEventContent diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt index ba79c9988f..e1fb9d06e6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt @@ -17,7 +17,7 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.matrix.api.media.MediaResolver +import io.element.android.libraries.matrix.ui.media.MediaRequestData open class TimelineItemImageContentProvider : PreviewParameterProvider { override val values: Sequence @@ -30,7 +30,7 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider suspend fun createDM(userId: UserId): Result fun startSync() fun stopSync() - fun mediaResolver(): MediaResolver fun sessionVerificationService(): SessionVerificationService fun pushersService(): PushersService fun notificationService(): NotificationService suspend fun logout() suspend fun loadUserDisplayName(): Result suspend fun loadUserAvatarURLString(): Result - suspend fun loadMediaContent(url: String): Result - suspend fun loadMediaThumbnail( - url: String, - width: Long, - height: Long - ): Result fun onSlidingSyncUpdate() diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt new file mode 100644 index 0000000000..4c068ab744 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.media + +import java.nio.file.Path + +interface MatrixMediaLoader { + /** + * @param url to fetch the content for. + * @return a [Result] of ByteArray. It contains the binary data for the media. + */ + suspend fun loadMediaContent(url: String): Result + + /** + * @param url to fetch the data for. + * @param width: the desired width for rescaling the media as thumbnail + * @param height: the desired height for rescaling the media as thumbnail + * @return a [Result] of ByteArray. It contains the binary data for the media. + */ + suspend fun loadMediaThumbnail(url: String, width: Long, height: Long): Result + + /** + * @param url to fetch the data for. + * @param mimeType: optional mime type + * @return a [Result] of [Path]. It's the path to the downloaded file. + */ + suspend fun loadMediaFile(url: String, mimeType: String?): Result +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaResolver.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaResolver.kt deleted file mode 100644 index 058c510ffd..0000000000 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaResolver.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.libraries.matrix.api.media - -interface MediaResolver { - - sealed interface Kind { - data class Thumbnail(val width: Int, val height: Int) : Kind { - constructor(size: Int) : this(size, size) - } - - object Content : Kind - } - - data class Meta( - val url: String?, - val kind: Kind - ) - - suspend fun resolve(url: String?, kind: Kind): Result - -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 63bd4e13a2..e6f6068c0d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -23,14 +23,14 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.createroom.RoomPreset import io.element.android.libraries.matrix.api.createroom.RoomVisibility -import io.element.android.libraries.matrix.api.media.MediaResolver +import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.verification.SessionVerificationService -import io.element.android.libraries.matrix.impl.media.RustMediaResolver +import io.element.android.libraries.matrix.impl.media.RustMediaLoader import io.element.android.libraries.matrix.impl.notification.RustNotificationService import io.element.android.libraries.matrix.impl.pushers.RustPushersService import io.element.android.libraries.matrix.impl.room.RustMatrixRoom @@ -53,7 +53,6 @@ import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder import org.matrix.rustcomponents.sdk.SlidingSyncMode import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters import org.matrix.rustcomponents.sdk.TaskHandle -import org.matrix.rustcomponents.sdk.mediaSourceFromUrl import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.io.File @@ -172,9 +171,12 @@ class RustMatrixClient constructor( override val invitesDataSource: RoomSummaryDataSource get() = rustInvitesDataSource + private val rustMediaLoader = RustMediaLoader(dispatchers, client) + override val mediaLoader: MatrixMediaLoader + get() = rustMediaLoader + private var slidingSyncObserverToken: TaskHandle? = null - private val mediaResolver = RustMediaResolver(this) private val isSyncing = AtomicBoolean(false) private val roomMembershipObserver = RoomMembershipObserver() @@ -254,8 +256,6 @@ class RustMatrixClient constructor( return createRoom(createRoomParams) } - override fun mediaResolver(): MediaResolver = mediaResolver - override fun sessionVerificationService(): SessionVerificationService = verificationService override fun pushersService(): PushersService = pushersService @@ -310,34 +310,6 @@ class RustMatrixClient constructor( } } - @OptIn(ExperimentalUnsignedTypes::class) - override suspend fun loadMediaContent(url: String): Result = - withContext(dispatchers.io) { - runCatching { - mediaSourceFromUrl(url).use { source -> - client.getMediaContent(source).toUByteArray().toByteArray() - } - } - } - - @OptIn(ExperimentalUnsignedTypes::class) - override suspend fun loadMediaThumbnail( - url: String, - width: Long, - height: Long - ): Result = - withContext(dispatchers.io) { - runCatching { - mediaSourceFromUrl(url).use { mediaSource -> - client.getMediaThumbnail( - mediaSource = mediaSource, - width = width.toULong(), - height = height.toULong() - ).toUByteArray().toByteArray() - } - } - } - override fun onSlidingSyncUpdate() { if (!verificationService.isReady.value) { try { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt new file mode 100644 index 0000000000..d4b8f4f72e --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt @@ -0,0 +1,75 @@ +/* + * 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.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.Client +import org.matrix.rustcomponents.sdk.mediaSourceFromUrl +import org.matrix.rustcomponents.sdk.use +import java.nio.file.Path +import kotlin.io.path.Path + +class RustMediaLoader( + private val dispatchers: CoroutineDispatchers, + private val innerClient: Client +) : MatrixMediaLoader { + + @OptIn(ExperimentalUnsignedTypes::class) + override suspend fun loadMediaContent(url: String): Result = + withContext(dispatchers.io) { + runCatching { + mediaSourceFromUrl(url).use { source -> + innerClient.getMediaContent(source).toUByteArray().toByteArray() + } + } + } + + @OptIn(ExperimentalUnsignedTypes::class) + override suspend fun loadMediaThumbnail( + url: String, + width: Long, + height: Long + ): Result = + withContext(dispatchers.io) { + runCatching { + mediaSourceFromUrl(url).use { mediaSource -> + innerClient.getMediaThumbnail( + mediaSource = mediaSource, + width = width.toULong(), + height = height.toULong() + ).toUByteArray().toByteArray() + } + } + } + + override suspend fun loadMediaFile(url: String, mimeType: String?): Result = + withContext(dispatchers.io) { + runCatching { + mediaSourceFromUrl(url).use { mediaSource -> + innerClient.getMediaFile( + source = mediaSource, + mimeType = mimeType ?: "application/octet-stream" + ).use { + Path(it.path()) + } + } + } + + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaResolver.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaResolver.kt deleted file mode 100644 index a708dc0d31..0000000000 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaResolver.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.MatrixClient -import io.element.android.libraries.matrix.api.media.MediaResolver -import java.lang.IllegalStateException - -internal class RustMediaResolver(private val client: MatrixClient) : MediaResolver { - - override suspend fun resolve(url: String?, kind: MediaResolver.Kind): Result { - if (url.isNullOrEmpty()) return Result.failure(IllegalStateException("The url is null or empty")) - return when (kind) { - is MediaResolver.Kind.Content -> client.loadMediaContent(url) - is MediaResolver.Kind.Thumbnail -> client.loadMediaThumbnail( - url, - kind.width.toLong(), - kind.height.toLong() - ) - } - } -} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index d33bd5c939..0167f2f846 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -21,14 +21,14 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters -import io.element.android.libraries.matrix.api.media.MediaResolver +import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.verification.SessionVerificationService -import io.element.android.libraries.matrix.test.media.FakeMediaResolver +import io.element.android.libraries.matrix.test.media.FakeMediaLoader import io.element.android.libraries.matrix.test.notification.FakeNotificationService import io.element.android.libraries.matrix.test.pushers.FakePushersService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom @@ -42,6 +42,7 @@ class FakeMatrixClient( private val userAvatarURLString: Result = Result.success(AN_AVATAR_URL), override val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(), override val invitesDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(), + override val mediaLoader: MatrixMediaLoader = FakeMediaLoader(), private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), private val pushersService: FakePushersService = FakePushersService(), private val notificationService: FakeNotificationService = FakeNotificationService(), @@ -77,10 +78,6 @@ class FakeMatrixClient( override fun stopSync() = Unit - override fun mediaResolver(): MediaResolver { - return FakeMediaResolver() - } - override suspend fun logout() { delay(100) logoutFailure?.let { throw it } @@ -96,14 +93,6 @@ class FakeMatrixClient( return userAvatarURLString } - override suspend fun loadMediaContent(url: String): Result { - return Result.success(ByteArray(0)) - } - - override suspend fun loadMediaThumbnail(url: String, width: Long, height: Long): Result { - return Result.success(ByteArray(0)) - } - override fun sessionVerificationService(): SessionVerificationService = sessionVerificationService override fun pushersService(): PushersService = pushersService diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt new file mode 100644 index 0000000000..fc14de03da --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt @@ -0,0 +1,50 @@ +/* + * 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.MatrixMediaLoader +import java.nio.file.Path +import kotlin.io.path.Path + +class FakeMediaLoader : MatrixMediaLoader { + + var shouldFail = false + + override suspend fun loadMediaContent(url: String): Result { + return if (shouldFail) { + Result.failure(RuntimeException()) + } else { + return Result.success(ByteArray(0)) + } + } + + override suspend fun loadMediaThumbnail(url: String, width: Long, height: Long): Result { + return if (shouldFail) { + Result.failure(RuntimeException()) + } else { + return Result.success(ByteArray(0)) + } + } + + override suspend fun loadMediaFile(url: String, mimeType: String?): Result { + return if (shouldFail) { + Result.failure(RuntimeException()) + } else { + return Result.success(Path("path")) + } + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaResolver.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaResolver.kt deleted file mode 100644 index c1b6e52003..0000000000 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaResolver.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.MediaResolver - -class FakeMediaResolver : MediaResolver { - - private var result: Result = Result.success(ByteArray(0)) - - fun givenResult(result: Result) { - this.result = result - } - - override suspend fun resolve(url: String?, kind: MediaResolver.Kind): Result { - return result - } -} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarDataExt.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt similarity index 74% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarDataExt.kt rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt index 6301503797..37a7f02922 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatarDataExt.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt @@ -17,9 +17,13 @@ package io.element.android.libraries.matrix.ui.media import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.matrix.api.media.MediaResolver -import kotlin.math.roundToInt +import kotlin.math.roundToLong -fun AvatarData.toMetadata(): MediaResolver.Meta { - return MediaResolver.Meta(url = url, kind = MediaResolver.Kind.Thumbnail(size.dp.value.roundToInt())) +fun AvatarData.toMediaRequestData(): MediaRequestData? { + return url?.let { + MediaRequestData( + url = it, + kind = MediaRequestData.Kind.Thumbnail(size.dp.value.roundToLong()) + ) + } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaFetcher.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt similarity index 60% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaFetcher.kt rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt index 4e1821e8cf..81b47c4022 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaFetcher.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt @@ -22,20 +22,20 @@ import coil.fetch.Fetcher import coil.request.Options import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.media.MediaResolver +import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import java.nio.ByteBuffer -internal class MediaFetcher( - private val mediaResolver: MediaResolver, - private val meta: MediaResolver.Meta, +internal class CoilMediaFetcher( + private val mediaLoader: MatrixMediaLoader, + private val mediaData: MediaRequestData?, private val options: Options, private val imageLoader: ImageLoader ) : Fetcher { override suspend fun fetch(): FetchResult? { - return mediaResolver.resolve(meta.url, meta.kind) - .map { byteArray -> - ByteBuffer.wrap(byteArray) + return loadMedia() + .map { data -> + ByteBuffer.wrap(data) }.map { byteBuffer -> imageLoader.components.newFetcher(byteBuffer, options, imageLoader)?.first?.fetch() } @@ -45,16 +45,28 @@ internal class MediaFetcher( ) } - class MetaFactory(private val client: MatrixClient) : - Fetcher.Factory { + private suspend fun loadMedia(): Result { + if (mediaData == null) return Result.failure(IllegalStateException("No media data to fetch.")) + return when (mediaData.kind) { + is MediaRequestData.Kind.Content -> mediaLoader.loadMediaContent(url = mediaData.url) + is MediaRequestData.Kind.Thumbnail -> mediaLoader.loadMediaThumbnail( + url = mediaData.url, + width = mediaData.kind.width, + height = mediaData.kind.height + ) + } + } + + class MediaRequestDataFactory(private val client: MatrixClient) : + Fetcher.Factory { override fun create( - data: MediaResolver.Meta, + data: MediaRequestData, options: Options, imageLoader: ImageLoader ): Fetcher { - return MediaFetcher( - mediaResolver = client.mediaResolver(), - meta = data, + return CoilMediaFetcher( + mediaLoader = client.mediaLoader, + mediaData = data, options = options, imageLoader = imageLoader ) @@ -63,14 +75,15 @@ internal class MediaFetcher( class AvatarFactory(private val client: MatrixClient) : Fetcher.Factory { + override fun create( data: AvatarData, options: Options, imageLoader: ImageLoader ): Fetcher { - return MediaFetcher( - mediaResolver = client.mediaResolver(), - meta = data.toMetadata(), + return CoilMediaFetcher( + mediaLoader = client.mediaLoader, + mediaData = data.toMediaRequestData(), options = options, imageLoader = imageLoader ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt index a52cfd380a..d1fab05544 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/ImageLoaderFactories.kt @@ -34,10 +34,10 @@ class LoggedInImageLoaderFactory @Inject constructor( .Builder(context) .okHttpClient(okHttpClient) .components { - add(AvatarKeyer()) - add(MediaKeyer()) - add(MediaFetcher.AvatarFactory(matrixClient)) - add(MediaFetcher.MetaFactory(matrixClient)) + add(AvatarDataKeyer()) + add(MediaRequestDataKeyer()) + add(CoilMediaFetcher.AvatarFactory(matrixClient)) + add(CoilMediaFetcher.MediaRequestDataFactory(matrixClient)) } .build() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaResolver.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt similarity index 62% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaResolver.kt rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt index ca840ee44f..f2f9bf9794 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaResolver.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -14,23 +14,19 @@ * limitations under the License. */ -package io.element.android.libraries.matrix.impl.media +package io.element.android.libraries.matrix.ui.media -interface MediaResolver { +data class MediaRequestData( + val url: String, + val kind: Kind +) { sealed interface Kind { - data class Thumbnail(val width: Int, val height: Int) : Kind { - constructor(size: Int) : this(size, size) + data class Thumbnail(val width: Long, val height: Long) : Kind { + constructor(size: Long) : this(size, size) } object Content : Kind } - - data class Meta( - val url: String?, - val kind: Kind - ) - - suspend fun resolve(url: String?, kind: Kind): ByteArray? - } + diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaKeyer.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt similarity index 72% rename from libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaKeyer.kt rename to libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt index 2d4ab683b1..56ff15fd44 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaKeyer.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt @@ -19,18 +19,17 @@ package io.element.android.libraries.matrix.ui.media import coil.key.Keyer import coil.request.Options import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.matrix.api.media.MediaResolver -internal class AvatarKeyer : Keyer { +internal class AvatarDataKeyer : Keyer { override fun key(data: AvatarData, options: Options): String? { - return data.toMetadata().toKey() + return data.toMediaRequestData()?.toKey() } } -internal class MediaKeyer : Keyer { - override fun key(data: MediaResolver.Meta, options: Options): String? { +internal class MediaRequestDataKeyer : Keyer { + override fun key(data: MediaRequestData, options: Options): String? { return data.toKey() } } -private fun MediaResolver.Meta.toKey() = "${url}_${kind}" +private fun MediaRequestData.toKey() = "${url}_${kind}" From baa7d3e5926ad1e23dd87d9979e4381f22bdc092 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 27 Apr 2023 15:35:25 +0200 Subject: [PATCH 04/48] Image: Try to handle blurhash --- .../components/event/TimelineItemImageView.kt | 28 +-- .../components/blurhash/BlurHashAsyncImage.kt | 79 +++++++ .../components/blurhash/BlurHashDecoder.kt | 204 ++++++++++++++++++ 3 files changed, 287 insertions(+), 24 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashAsyncImage.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashDecoder.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index ae2d18f541..fe0e4abba0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -19,25 +19,17 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import coil.compose.AsyncImage -import coil.request.ImageRequest import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider +import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground -import timber.log.Timber @Composable fun TimelineItemImageView( @@ -55,22 +47,10 @@ fun TimelineItemImageView( .aspectRatio(content.aspectRatio), contentAlignment = Alignment.Center, ) { - val isLoading = rememberSaveable(content.mediaRequestData) { mutableStateOf(true) } - val context = LocalContext.current - val model = ImageRequest.Builder(context) - .data(content.mediaRequestData) - .build() - - AsyncImage( - model = model, - contentDescription = null, - placeholder = debugPlaceholderBackground(ColorPainter(MaterialTheme.colorScheme.surfaceVariant)), + BlurHashAsyncImage( + blurHash = content.blurhash, + model = content.mediaRequestData, contentScale = ContentScale.Crop, - onSuccess = { - Timber.v("OnSuccess = ${it.result.dataSource}") - isLoading.value = false - }, - onError = { Timber.e(it.result.throwable) } ) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashAsyncImage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashAsyncImage.kt new file mode 100644 index 0000000000..a7a432163c --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashAsyncImage.kt @@ -0,0 +1,79 @@ +/* + * 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.components.blurhash + +import android.graphics.Bitmap +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import coil.compose.SubcomposeAsyncImage + +@Composable +fun BlurHashAsyncImage( + model: Any?, + blurHash: String?, + modifier: Modifier = Modifier, + contentScale: ContentScale = ContentScale.Fit, + contentDescription: String? = null, +) { + SubcomposeAsyncImage( + model = model, + modifier = modifier, + contentScale = contentScale, + contentDescription = contentDescription, + loading = { + BlurHashImage( + blurHash = blurHash, + contentScale = contentScale, + contentDescription = "Loading placeholder" + ) + }, + ) +} + +@Composable +fun BlurHashImage( + blurHash: String?, + modifier: Modifier = Modifier, + contentDescription: String? = null, + contentScale: ContentScale = ContentScale.Fit, +) { + val bitmapState = remember { + mutableStateOf(null) + } + DisposableEffect(blurHash) { + // Build a small blurhash image so that it's fast + bitmapState.value = BlurHashDecoder.decode(blurHash, 10, 10) + onDispose { + bitmapState.value?.recycle() + } + } + bitmapState.value?.let { bitmap -> + Image( + modifier = modifier.fillMaxSize(), + bitmap = bitmap.asImageBitmap(), + contentScale = contentScale, + contentDescription = contentDescription + ) + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashDecoder.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashDecoder.kt new file mode 100644 index 0000000000..ec44a61ff3 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashDecoder.kt @@ -0,0 +1,204 @@ +/* + * 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.components.blurhash + +import android.graphics.Bitmap +import android.graphics.Color +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.withSign + +/** + * Extracted from https://github.com/woltapp/blurhash/blob/master/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt + */ +object BlurHashDecoder { + + // cache Math.cos() calculations to improve performance. + // The number of calculations can be huge for many bitmaps: width * height * numCompX * numCompY * 2 * nBitmaps + // the cache is enabled by default, it is recommended to disable it only when just a few images are displayed + private val cacheCosinesX = HashMap() + private val cacheCosinesY = HashMap() + + /** + * Clear calculations stored in memory cache. + * The cache is not big, but will increase when many image sizes are used, + * if the app needs memory it is recommended to clear it. + */ + fun clearCache() { + cacheCosinesX.clear() + cacheCosinesY.clear() + } + + /** + * Decode a blur hash into a new bitmap. + * + * @param useCache use in memory cache for the calculated math, reused by images with same size. + * if the cache does not exist yet it will be created and populated with new calculations. + * By default it is true. + */ + fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useCache: Boolean = true): Bitmap? { + if (blurHash == null || blurHash.length < 6) { + return null + } + val numCompEnc = decode83(blurHash, 0, 1) + val numCompX = (numCompEnc % 9) + 1 + val numCompY = (numCompEnc / 9) + 1 + if (blurHash.length != 4 + 2 * numCompX * numCompY) { + return null + } + val maxAcEnc = decode83(blurHash, 1, 2) + val maxAc = (maxAcEnc + 1) / 166f + val colors = Array(numCompX * numCompY) { i -> + if (i == 0) { + val colorEnc = decode83(blurHash, 2, 6) + decodeDc(colorEnc) + } else { + val from = 4 + i * 2 + val colorEnc = decode83(blurHash, from, from + 2) + decodeAc(colorEnc, maxAc * punch) + } + } + return composeBitmap(width, height, numCompX, numCompY, colors, useCache) + } + + private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int { + var result = 0 + for (i in from until to) { + val index = charMap[str[i]] ?: -1 + if (index != -1) { + result = result * 83 + index + } + } + return result + } + + private fun decodeDc(colorEnc: Int): FloatArray { + val r = colorEnc shr 16 + val g = (colorEnc shr 8) and 255 + val b = colorEnc and 255 + return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)) + } + + private fun srgbToLinear(colorEnc: Int): Float { + val v = colorEnc / 255f + return if (v <= 0.04045f) { + (v / 12.92f) + } else { + ((v + 0.055f) / 1.055f).pow(2.4f) + } + } + + private fun decodeAc(value: Int, maxAc: Float): FloatArray { + val r = value / (19 * 19) + val g = (value / 19) % 19 + val b = value % 19 + return floatArrayOf( + signedPow2((r - 9) / 9.0f) * maxAc, + signedPow2((g - 9) / 9.0f) * maxAc, + signedPow2((b - 9) / 9.0f) * maxAc + ) + } + + private fun signedPow2(value: Float) = value.pow(2f).withSign(value) + + private fun composeBitmap( + width: Int, height: Int, + numCompX: Int, numCompY: Int, + colors: Array, + useCache: Boolean + ): Bitmap { + // use an array for better performance when writing pixel colors + val imageArray = IntArray(width * height) + val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX) + val cosinesX = getArrayForCosinesX(calculateCosX, width, numCompX) + val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY) + val cosinesY = getArrayForCosinesY(calculateCosY, height, numCompY) + for (y in 0 until height) { + for (x in 0 until width) { + var r = 0f + var g = 0f + var b = 0f + for (j in 0 until numCompY) { + for (i in 0 until numCompX) { + val cosX = cosinesX.getCos(calculateCosX, i, numCompX, x, width) + val cosY = cosinesY.getCos(calculateCosY, j, numCompY, y, height) + val basis = (cosX * cosY).toFloat() + val color = colors[j * numCompX + i] + r += color[0] * basis + g += color[1] * basis + b += color[2] * basis + } + } + imageArray[x + width * y] = Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) + } + } + return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) + } + + private fun getArrayForCosinesY(calculate: Boolean, height: Int, numCompY: Int) = when { + calculate -> { + DoubleArray(height * numCompY).also { + cacheCosinesY[height * numCompY] = it + } + } + else -> { + cacheCosinesY[height * numCompY]!! + } + } + + private fun getArrayForCosinesX(calculate: Boolean, width: Int, numCompX: Int) = when { + calculate -> { + DoubleArray(width * numCompX).also { + cacheCosinesX[width * numCompX] = it + } + } + else -> cacheCosinesX[width * numCompX]!! + } + + private fun DoubleArray.getCos( + calculate: Boolean, + x: Int, + numComp: Int, + y: Int, + size: Int + ): Double { + if (calculate) { + this[x + numComp * y] = cos(Math.PI * y * x / size) + } + return this[x + numComp * y] + } + + private fun linearToSrgb(value: Float): Int { + val v = value.coerceIn(0f, 1f) + return if (v <= 0.0031308f) { + (v * 12.92f * 255f + 0.5f).toInt() + } else { + ((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt() + } + } + + private val charMap = listOf( + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', + 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',', + '-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~' + ) + .mapIndexed { i, c -> c to i } + .toMap() + +} From ed10fc6651cb76dd1703aee90233b4e2d0b3ccce Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 2 May 2023 19:21:39 +0200 Subject: [PATCH 05/48] MediaViewer: first step --- features/messages/impl/build.gradle.kts | 1 + .../impl/DefaultMessagesEntryPoint.kt | 4 +- .../messages/impl/MessagesFlowNode.kt | 107 ++++++++++++++++++ .../features/messages/impl/MessagesNode.kt | 17 ++- .../features/messages/impl/MessagesView.kt | 10 +- .../impl/media/viewer/MediaViewerEvents.kt | 22 ++++ .../impl/media/viewer/MediaViewerNode.kt | 49 ++++++++ .../impl/media/viewer/MediaViewerState.kt | 23 ++++ .../media/viewer/MediaViewerStateProvider.kt | 38 +++++++ .../impl/media/viewer/MediaViewerView.kt | 97 ++++++++++++++++ .../media/viewer/model/MediaContentUiModel.kt | 40 +++++++ 11 files changed, 397 insertions(+), 11 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/model/MediaContentUiModel.kt diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 3bd73d5ed2..ea037f11f4 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -18,6 +18,7 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.anvil) alias(libs.plugins.ksp) + id("kotlin-parcelize") } android { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt index 6b0df92f09..abf451b4b6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/DefaultMessagesEntryPoint.kt @@ -18,12 +18,10 @@ package io.element.android.features.messages.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope -import io.element.android.libraries.matrix.api.core.RoomId import javax.inject.Inject @ContributesBinding(AppScope::class) @@ -33,6 +31,6 @@ class DefaultMessagesEntryPoint @Inject constructor() : MessagesEntryPoint { buildContext: BuildContext, callback: MessagesEntryPoint.Callback ): Node { - return parentNode.createNode(buildContext, listOf(callback)) + return parentNode.createNode(buildContext, listOf(callback)) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt new file mode 100644 index 0000000000..95a5d4ec9e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -0,0 +1,107 @@ +/* + * 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 + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.messages.api.MessagesEntryPoint +import io.element.android.features.messages.impl.media.viewer.MediaViewerNode +import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent +import io.element.android.libraries.architecture.BackstackNode +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.RoomScope +import kotlinx.android.parcel.Parcelize + +@ContributesNode(RoomScope::class) +class MessagesFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : BackstackNode( + backstack = BackStack( + initialElement = NavTarget.Messages, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +) { + + sealed interface NavTarget : Parcelable { + @Parcelize + object Messages : NavTarget + + @Parcelize + data class MediaViewer(val mediaContent: MediaContentUiModel) : NavTarget + } + + private val callback = plugins().firstOrNull() + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + is NavTarget.Messages -> { + val callback = object : MessagesNode.Callback { + override fun onRoomDetailsClicked() { + callback?.onRoomDetailsClicked() + } + + override fun onEventClicked(event: TimelineItem.Event) { + processEventClicked(event) + } + } + createNode(buildContext, listOf(callback)) + } + is NavTarget.MediaViewer -> { + val inputs = MediaViewerNode.Inputs(navTarget.mediaContent) + createNode(buildContext, listOf(inputs)) + } + } + } + + private fun processEventClicked(event: TimelineItem.Event) { + when (event.content) { + is TimelineItemImageContent -> { + val mediaContent = MediaContentUiModel.Image( + body = event.content.body, + url = event.content.mediaRequestData.url, + blurhash = event.content.blurhash + ) + backstack.push(NavTarget.MediaViewer(mediaContent)) + } + else -> Unit + } + } + + @Composable + override fun View(modifier: Modifier) { + Children( + navModel = backstack, + modifier = modifier, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 9665907eb2..fb0804abd0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -25,9 +25,8 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.messages.api.MessagesEntryPoint +import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.core.RoomId @ContributesNode(RoomScope::class) class MessagesNode @AssistedInject constructor( @@ -36,12 +35,21 @@ class MessagesNode @AssistedInject constructor( private val presenter: MessagesPresenter, ) : Node(buildContext, plugins = plugins) { - private val callback = plugins().firstOrNull() + private val callback = plugins().firstOrNull() + + interface Callback : Plugin { + fun onRoomDetailsClicked() + fun onEventClicked(event: TimelineItem.Event) + } private fun onRoomDetailsClicked() { callback?.onRoomDetailsClicked() } + private fun onEventClicked(event: TimelineItem.Event) { + callback?.onEventClicked(event) + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -49,7 +57,8 @@ class MessagesNode @AssistedInject constructor( state = state, onBackPressed = this::navigateUp, onRoomDetailsClicked = this::onRoomDetailsClicked, - modifier = modifier + onEventClicked = this::onEventClicked, + modifier = modifier, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 132d139174..7dc66d076e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -60,6 +60,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc import io.element.android.features.messages.impl.textcomposer.MessageComposerView import io.element.android.features.messages.impl.timeline.TimelineView import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -70,16 +71,16 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.LogCompositions -import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView import kotlinx.coroutines.launch import timber.log.Timber @Composable fun MessagesView( state: MessagesState, + onBackPressed: () -> Unit, + onRoomDetailsClicked: () -> Unit, + onEventClicked: (event: TimelineItem.Event) -> Unit, modifier: Modifier = Modifier, - onBackPressed: () -> Unit = {}, - onRoomDetailsClicked: () -> Unit = {}, ) { LogCompositions(tag = "MessagesScreen", msg = "Root") val itemActionsBottomSheetState = rememberModalBottomSheetState( @@ -93,6 +94,7 @@ fun MessagesView( fun onMessageClicked(event: TimelineItem.Event) { Timber.v("OnMessageClicked= ${event.id}") + onEventClicked(event) } fun onMessageLongClicked(event: TimelineItem.Event) { @@ -228,5 +230,5 @@ internal fun MessagesViewDarkPreview(@PreviewParameter(MessagesStateProvider::cl @Composable private fun ContentToPreview(state: MessagesState) { - MessagesView(state) + MessagesView(state, {}, {}, {}) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt new file mode 100644 index 0000000000..2f4398d570 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt @@ -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 + +// TODO Add your events or remove the file completely if no events +sealed interface MediaViewerEvents { + object MyEvent : MediaViewerEvents +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt new file mode 100644 index 0000000000..439d5c1ea1 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt @@ -0,0 +1,49 @@ +/* + * 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 + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.RoomScope + +@ContributesNode(RoomScope::class) +class MediaViewerNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : Node(buildContext, plugins = plugins) { + + data class Inputs(val mediaContent: MediaContentUiModel) : NodeInputs + + private val inputs: Inputs = inputs() + + @Composable + override fun View(modifier: Modifier) { + MediaViewerView( + state = MediaViewerState(inputs.mediaContent), + modifier = modifier + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt new file mode 100644 index 0000000000..a8d02dea15 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt @@ -0,0 +1,23 @@ +/* + * 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 + +import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel + +data class MediaViewerState( + val mediaContent: MediaContentUiModel +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt new file mode 100644 index 0000000000..61cb4d36a1 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt @@ -0,0 +1,38 @@ +/* + * 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 + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel + +open class MediaViewerStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aMediaViewerState(), + ) +} + +fun aMediaViewerState() = MediaViewerState( + mediaContent = aMediaImage(), +) + +private fun aMediaImage() = MediaContentUiModel.Image( + body = "a body", + url = "", + blurhash = null, +) + diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt new file mode 100644 index 0000000000..0ce7c09ea5 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -0,0 +1,97 @@ +/* + * 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 + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel +import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight + +@Composable +fun MediaViewerView( + state: MediaViewerState, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center, + ) { + when (state.mediaContent) { + is MediaContentUiModel.Image -> MediaImageViewer(state.mediaContent) + is MediaContentUiModel.Video -> MediaVideoViewer(state.mediaContent) + } + } +} + +@Composable +private fun MediaImageViewer( + image: MediaContentUiModel.Image, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + BlurHashAsyncImage( + blurHash = image.blurhash, + model = image.mediaRequestData, + contentScale = ContentScale.Crop, + ) + } +} + +@Composable +private fun MediaVideoViewer( + video: MediaContentUiModel.Video, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + + } +} + +@Preview +@Composable +fun MediaViewerViewLightPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun MediaViewerViewDarkPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: MediaViewerState) { + MediaViewerView( + state = state, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/model/MediaContentUiModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/model/MediaContentUiModel.kt new file mode 100644 index 0000000000..0f409364d2 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/model/MediaContentUiModel.kt @@ -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.features.messages.impl.media.viewer.model + +import android.os.Parcelable +import io.element.android.libraries.matrix.ui.media.MediaRequestData +import kotlinx.android.parcel.Parcelize + +sealed interface MediaContentUiModel : Parcelable { + + @Parcelize + data class Image( + val body: String, + val url: String, + val blurhash: String?, + ) : MediaContentUiModel { + val mediaRequestData = MediaRequestData( + url = url, kind = MediaRequestData.Kind.Content + ) + } + + @Parcelize + data class Video( + val body: String, + ) : MediaContentUiModel +} From 28770afac0a2463c772a0535f1e58c32a45cc91c Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 2 May 2023 21:13:21 +0200 Subject: [PATCH 06/48] Image: play with a ZoomableBox --- .../impl/media/viewer/MediaViewerView.kt | 19 +++- .../designsystem/components/ZoomableBox.kt | 106 ++++++++++++++++++ 2 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ZoomableBox.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt index 0ce7c09ea5..38377ea05e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -17,16 +17,28 @@ package io.element.android.features.messages.impl.media.viewer import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.calculatePan +import androidx.compose.foundation.gestures.calculateZoom import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +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.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel +import io.element.android.libraries.designsystem.components.ZoomableBox import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -54,14 +66,14 @@ private fun MediaImageViewer( image: MediaContentUiModel.Image, modifier: Modifier = Modifier, ) { - Box( + ZoomableBox( modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center ) { BlurHashAsyncImage( blurHash = image.blurhash, + modifier = Modifier.fillMaxSize().zoomable(), model = image.mediaRequestData, - contentScale = ContentScale.Crop, + contentScale = ContentScale.Fit, ) } } @@ -79,6 +91,7 @@ private fun MediaVideoViewer( } } + @Preview @Composable fun MediaViewerViewLightPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ZoomableBox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ZoomableBox.kt new file mode 100644 index 0000000000..d9159da3c8 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ZoomableBox.kt @@ -0,0 +1,106 @@ +/* + * 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.components + +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.LayoutScopeMarker +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntSize + +@Composable +fun ZoomableBox( + modifier: Modifier = Modifier, + minScale: Float = 1f, + maxScale: Float = 5f, + content: @Composable ZoomableBoxScope.() -> Unit +) { + var scale by remember { mutableStateOf(1f) } + var offsetX by remember { mutableStateOf(0f) } + var offsetY by remember { mutableStateOf(0f) } + var size by remember { mutableStateOf(IntSize.Zero) } + Box( + modifier = modifier + .clip(RectangleShape) + .onSizeChanged { size = it } + .pointerInput(Unit) { + detectTransformGestures { _, pan, zoom, _ -> + scale = maxOf(minScale, minOf(scale * zoom, maxScale)) + val maxX = (size.width * (scale - 1)) / 2 + val minX = -maxX + offsetX = maxOf(minX, minOf(maxX, offsetX + pan.x)) + val maxY = (size.height * (scale - 1)) / 2 + val minY = -maxY + offsetY = maxOf(minY, minOf(maxY, offsetY + pan.y)) + } + } + .pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { + offsetX = 0f + offsetY = 0f + scale = if (scale > minScale) { + minScale + } else { + maxScale / 2f + } + + } + ) + } + ) { + DefaultZoomableBoxScope(this, scale, offsetX, offsetY).content() + } +} + +@LayoutScopeMarker +@Immutable +interface ZoomableBoxScope : BoxScope { + @Stable + fun Modifier.zoomable(): Modifier +} + +private class DefaultZoomableBoxScope( + private val parentScope: BoxScope, + private val scale: Float, + private val offsetX: Float, + private val offsetY: Float +) : ZoomableBoxScope, BoxScope by parentScope { + + override fun Modifier.zoomable(): Modifier { + return graphicsLayer( + scaleX = scale, + scaleY = scale, + translationX = offsetX, + translationY = offsetY + ) + } +} From 4236b697050253b29aaca3d7163d8afd5f85e8c8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 5 May 2023 19:47:10 +0200 Subject: [PATCH 07/48] Introduce MatrixMediaSource --- .../event/TimelineItemContentView.kt | 5 ++ .../components/event/TimelineItemImageView.kt | 17 ++-- .../components/event/TimelineItemVideoView.kt | 81 +++++++++++++++++++ .../TimelineItemContentMessageFactory.kt | 30 +++++-- .../model/event/TimelineItemImageContent.kt | 8 +- .../event/TimelineItemImageContentProvider.kt | 7 +- .../model/event/TimelineItemVideoContent.kt | 33 ++++++++ .../event/TimelineItemVideoContentProvider.kt | 42 ++++++++++ .../DefaultRoomLastMessageFormatterTests.kt | 10 ++- .../libraries/matrix/api/media/FileInfo.kt | 2 +- .../libraries/matrix/api/media/ImageInfo.kt | 2 +- .../matrix/api/media/MatrixMediaLoader.kt | 10 +-- .../matrix/api/media/MatrixMediaSource.kt | 25 ++++++ .../libraries/matrix/api/media/VideoInfo.kt | 2 +- .../api/timeline/item/event/EventContent.kt | 13 +-- .../libraries/matrix/impl/media/FileInfo.kt | 4 +- .../libraries/matrix/impl/media/ImageInfo.kt | 2 +- .../matrix/impl/media/MediaSource.kt | 5 +- .../matrix/impl/media/RustMediaLoader.kt | 20 ++--- .../libraries/matrix/impl/media/VideoInfo.kt | 2 +- .../timeline/item/event/EventMessageMapper.kt | 9 +-- .../item/event/TimelineEventContentMapper.kt | 4 +- .../matrix/test/media/FakeMediaLoader.kt | 13 +-- .../matrix/ui/media/AvatatarDataExt.kt | 13 ++- .../matrix/ui/media/CoilMediaFetcher.kt | 12 +-- .../matrix/ui/media/MediaRequestData.kt | 4 +- .../matrix/ui/media/MediaRequestDataKeyer.kt | 7 +- 27 files changed, 298 insertions(+), 84 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaSource.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt index 5171c92c40..6ef3965c5c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt @@ -27,6 +27,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent private fun Modifier.defaultContentPadding(): Modifier = padding( horizontal = 12.dp, vertical = 6.dp @@ -64,5 +65,9 @@ fun TimelineItemEventContentView( content = content, modifier = modifier, ) + is TimelineItemVideoContent -> TimelineItemVideoView( + content = content, + modifier = modifier + ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index fe0e4abba0..7fa2076574 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -18,39 +18,38 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.matrix.ui.media.MediaRequestData +import kotlin.math.min @Composable fun TimelineItemImageView( content: TimelineItemImageContent, modifier: Modifier = Modifier, ) { - val widthPercent = if (content.aspectRatio > 1f) { - 1f - } else { - 0.7f - } + val maxHeight = min(300, content.height ?: Int.MAX_VALUE) Box( modifier = modifier - .fillMaxWidth(widthPercent) + .heightIn(max = maxHeight.dp) .aspectRatio(content.aspectRatio), contentAlignment = Alignment.Center, ) { BlurHashAsyncImage( blurHash = content.blurhash, - model = content.mediaRequestData, - contentScale = ContentScale.Crop, + model = MediaRequestData(content.mediaSource, MediaRequestData.Kind.Content), + contentScale = ContentScale.Fit, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt new file mode 100644 index 0000000000..5dec3717c8 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components.event + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.heightIn +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent +import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.matrix.ui.media.MediaRequestData +import kotlin.math.min + +@Composable +fun TimelineItemVideoView( + content: TimelineItemVideoContent, + modifier: Modifier = Modifier, +) { + val maxHeight = min(300, content.height ?: Int.MAX_VALUE) + Box( + modifier = modifier + .heightIn(max = maxHeight.dp) + .aspectRatio(content.aspectRatio), + contentAlignment = Alignment.Center, + ) { + BlurHashAsyncImage( + blurHash = content.blurhash, + model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.Content), + contentScale = ContentScale.Fit, + ) + Image( + painterResource(id = androidx.media3.ui.R.drawable.exo_ic_play_circle_filled), + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground) + ) + + } +} + +@Preview +@Composable +internal fun TimelineItemVideoViewLightPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) = + ElementPreviewLight { ContentToPreview(content) } + +@Preview +@Composable +internal fun TimelineItemVideoViewDarkPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) = + ElementPreviewDark { ContentToPreview(content) } + +@Composable +private fun ContentToPreview(content: TimelineItemImageContent) { + TimelineItemImageView(content) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index 57d6489592..857049d1be 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -22,13 +22,14 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.util.toHtmlDocument import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType -import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import javax.inject.Inject class TimelineItemContentMessageFactory @Inject constructor() { @@ -49,10 +50,29 @@ class TimelineItemContentMessageFactory @Inject constructor() { } TimelineItemImageContent( body = messageType.body, - mediaRequestData = MediaRequestData( - url = messageType.url, - kind = MediaRequestData.Kind.Content - ), + height = messageType.info?.height?.toInt(), + width = messageType.info?.width?.toInt(), + mediaSource = messageType.source, + blurhash = messageType.info?.blurhash, + aspectRatio = aspectRatio + ) + } + is VideoMessageType -> { + val height = messageType.info?.height?.toFloat() + val width = messageType.info?.width?.toFloat() + val aspectRatio = if (height != null && width != null) { + width / height + } else { + 0.7f + } + TimelineItemVideoContent( + body = messageType.body, + thumbnailSource = messageType.info?.thumbnailSource, + videoSource = messageType.source, + mimetype = messageType.info?.mimetype, + width = messageType.info?.width?.toInt(), + height = messageType.info?.height?.toInt(), + duration = messageType.info?.duration ?: 0L, blurhash = messageType.info?.blurhash, aspectRatio = aspectRatio ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt index d6069e2020..9f70d16de4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt @@ -16,13 +16,15 @@ package io.element.android.features.messages.impl.timeline.model.event -import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.matrix.api.media.MatrixMediaSource data class TimelineItemImageContent( val body: String, - val mediaRequestData: MediaRequestData, + val mediaSource: MatrixMediaSource, val blurhash: String?, + val width: Int?, + val height: Int?, val aspectRatio: Float -) : TimelineItemEventContent{ +) : TimelineItemEventContent { override val type: String = "TimelineItemImageContent" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt index e1fb9d06e6..a43551cd2d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.media.MatrixMediaSource import io.element.android.libraries.matrix.ui.media.MediaRequestData open class TimelineItemImageContentProvider : PreviewParameterProvider { @@ -30,7 +31,9 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aTimelineItemVideoContent(), + aTimelineItemVideoContent().copy(aspectRatio = 1.0f), + aTimelineItemVideoContent().copy(aspectRatio = 1.5f), + ) +} + +fun aTimelineItemVideoContent() = TimelineItemVideoContent( + body = "a video", + thumbnailSource = MatrixMediaSource(url = ""), + blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr", + aspectRatio = 0.5f, + duration = 0, + videoSource = MatrixMediaSource(""), + height = null, + width = null, + mimetype = null +) diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/DefaultRoomLastMessageFormatterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/DefaultRoomLastMessageFormatterTests.kt index d4c2ca7dd2..641753db76 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/DefaultRoomLastMessageFormatterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/DefaultRoomLastMessageFormatterTests.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.text.AnnotatedString import com.google.common.truth.Truth import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.ImageInfo +import io.element.android.libraries.matrix.api.media.MatrixMediaSource import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EventContent @@ -145,12 +146,13 @@ class DefaultRoomLastMessageFormatterTests { fun createMessageContent(type: MessageType): MessageContent { return MessageContent(body, null, false, type) } + val sharedContentMessagesTypes = arrayOf( TextMessageType(body, null), - VideoMessageType(body, "url", null), - AudioMessageType(body, "url", null), - ImageMessageType(body, "url", null), - FileMessageType(body, "url", null), + VideoMessageType(body, MatrixMediaSource("url"), null), + AudioMessageType(body, MatrixMediaSource("url"), null), + ImageMessageType(body, MatrixMediaSource("url"), null), + FileMessageType(body, MatrixMediaSource("url"), null), NoticeMessageType(body, null), EmoteMessageType(body, null), ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/FileInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/FileInfo.kt index fc591a5078..b989a621fd 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/FileInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/FileInfo.kt @@ -20,5 +20,5 @@ data class FileInfo( val mimetype: String?, val size: Long?, val thumbnailInfo: ThumbnailInfo?, - val thumbnailUrl: String? + val thumbnailSource: MatrixMediaSource? ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/ImageInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/ImageInfo.kt index 540627470e..a1361082c8 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/ImageInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/ImageInfo.kt @@ -22,6 +22,6 @@ data class ImageInfo( val mimetype: String?, val size: Long?, val thumbnailInfo: ThumbnailInfo?, - val thumbnailUrl: String?, + val thumbnailSource: MatrixMediaSource?, val blurhash: String? ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt index 4c068ab744..43ab98c3b9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt @@ -16,14 +16,14 @@ package io.element.android.libraries.matrix.api.media -import java.nio.file.Path +import android.net.Uri interface MatrixMediaLoader { /** * @param url to fetch the content for. * @return a [Result] of ByteArray. It contains the binary data for the media. */ - suspend fun loadMediaContent(url: String): Result + suspend fun loadMediaContent(source: MatrixMediaSource): Result /** * @param url to fetch the data for. @@ -31,12 +31,12 @@ interface MatrixMediaLoader { * @param height: the desired height for rescaling the media as thumbnail * @return a [Result] of ByteArray. It contains the binary data for the media. */ - suspend fun loadMediaThumbnail(url: String, width: Long, height: Long): Result + suspend fun loadMediaThumbnail(source: MatrixMediaSource, width: Long, height: Long): Result /** * @param url to fetch the data for. * @param mimeType: optional mime type - * @return a [Result] of [Path]. It's the path to the downloaded file. + * @return a [Result] of [Uri]. It's the uri of the downloaded file. */ - suspend fun loadMediaFile(url: String, mimeType: String?): Result + suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaSource.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaSource.kt new file mode 100644 index 0000000000..22092c1afd --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaSource.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.media + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class MatrixMediaSource( + val url: String +) : Parcelable diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt index c2d74fc2f6..ca24a3303f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt @@ -23,6 +23,6 @@ data class VideoInfo( val mimetype: String?, val size: Long?, val thumbnailInfo: ThumbnailInfo?, - val thumbnailUrl: String?, + val thumbnailSource: MatrixMediaSource?, val blurhash: String? ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt index 63d3ce3911..31bdfee4fa 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo +import io.element.android.libraries.matrix.api.media.MatrixMediaSource import io.element.android.libraries.matrix.api.media.VideoInfo sealed interface EventContent @@ -106,25 +107,25 @@ data class EmoteMessageType( data class ImageMessageType( val body: String, - val url: String, + val source: MatrixMediaSource, val info: ImageInfo? ) : MessageType data class AudioMessageType( - var body: String, - var url: String, - var info: AudioInfo? + val body: String, + val source: MatrixMediaSource, + val info: AudioInfo? ) : MessageType data class VideoMessageType( val body: String, - val url: String, + val source: MatrixMediaSource, val info: VideoInfo? ) : MessageType data class FileMessageType( val body: String, - val url: String, + val source: MatrixMediaSource, val info: FileInfo? ) : MessageType diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/FileInfo.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/FileInfo.kt index 98c96c4d9a..d9f8f74df1 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/FileInfo.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/FileInfo.kt @@ -17,13 +17,11 @@ package io.element.android.libraries.matrix.impl.media import io.element.android.libraries.matrix.api.media.FileInfo -import io.element.android.libraries.matrix.api.media.ThumbnailInfo import org.matrix.rustcomponents.sdk.FileInfo as RustFileInfo -import org.matrix.rustcomponents.sdk.ThumbnailInfo as RustThumbnailInfo fun RustFileInfo.map(): FileInfo = FileInfo( mimetype = mimetype, size = size?.toLong(), thumbnailInfo = thumbnailInfo?.map(), - thumbnailUrl = thumbnailSource?.useUrl() + thumbnailSource = thumbnailSource?.map() ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/ImageInfo.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/ImageInfo.kt index 27ab6d656a..3716912c79 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/ImageInfo.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/ImageInfo.kt @@ -25,6 +25,6 @@ fun RustImageInfo.map(): ImageInfo = ImageInfo( mimetype = mimetype, size = size?.toLong(), thumbnailInfo = thumbnailInfo?.map(), - thumbnailUrl = thumbnailSource?.useUrl(), + thumbnailSource = thumbnailSource?.map(), blurhash = blurhash ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaSource.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaSource.kt index 2fc50611e8..4d3dc6603f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaSource.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaSource.kt @@ -16,7 +16,8 @@ package io.element.android.libraries.matrix.impl.media -import org.matrix.rustcomponents.sdk.MediaSource +import io.element.android.libraries.matrix.api.media.MatrixMediaSource import org.matrix.rustcomponents.sdk.use +import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource -fun MediaSource.useUrl(): String = use { it.url() } +fun RustMediaSource.map(): MatrixMediaSource = use { MatrixMediaSource(it.url()) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt index 6d7d48db24..52d4d7ccf4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt @@ -16,14 +16,15 @@ 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.matrix.api.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.media.MatrixMediaSource import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.mediaSourceFromUrl import org.matrix.rustcomponents.sdk.use -import java.nio.file.Path -import kotlin.io.path.Path +import java.io.File class RustMediaLoader( private val dispatchers: CoroutineDispatchers, @@ -31,10 +32,10 @@ class RustMediaLoader( ) : MatrixMediaLoader { @OptIn(ExperimentalUnsignedTypes::class) - override suspend fun loadMediaContent(url: String): Result = + override suspend fun loadMediaContent(source: MatrixMediaSource): Result = withContext(dispatchers.io) { runCatching { - mediaSourceFromUrl(url).use { source -> + mediaSourceFromUrl(source.url).use { source -> innerClient.getMediaContent(source).toUByteArray().toByteArray() } } @@ -42,13 +43,13 @@ class RustMediaLoader( @OptIn(ExperimentalUnsignedTypes::class) override suspend fun loadMediaThumbnail( - url: String, + source: MatrixMediaSource, width: Long, height: Long ): Result = withContext(dispatchers.io) { runCatching { - mediaSourceFromUrl(url).use { mediaSource -> + mediaSourceFromUrl(source.url).use { mediaSource -> innerClient.getMediaThumbnail( mediaSource = mediaSource, width = width.toULong(), @@ -58,15 +59,16 @@ class RustMediaLoader( } } - override suspend fun loadMediaFile(url: String, mimeType: String?): Result = + override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result = withContext(dispatchers.io) { runCatching { - mediaSourceFromUrl(url).use { mediaSource -> + mediaSourceFromUrl(source.url).use { mediaSource -> innerClient.getMediaFile( mediaSource = mediaSource, mimeType = mimeType ?: "application/octet-stream" ).use { - Path(it.path()) + val file = File(it.path()) + Uri.fromFile(file) } } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/VideoInfo.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/VideoInfo.kt index 0db364f544..09e717c85d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/VideoInfo.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/VideoInfo.kt @@ -26,6 +26,6 @@ fun RustVideoInfo.map(): VideoInfo = VideoInfo( mimetype = mimetype, size = size?.toLong(), thumbnailInfo = thumbnailInfo?.map(), - thumbnailUrl = thumbnailSource?.useUrl(), + thumbnailSource = thumbnailSource?.map(), blurhash = blurhash ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index a000783e86..439ce46b1d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -29,7 +29,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType import io.element.android.libraries.matrix.impl.media.map -import io.element.android.libraries.matrix.impl.media.useUrl import org.matrix.rustcomponents.sdk.Message import org.matrix.rustcomponents.sdk.MessageType import org.matrix.rustcomponents.sdk.use @@ -42,13 +41,13 @@ class EventMessageMapper { val type = message.msgtype().use { type -> when (type) { is MessageType.Audio -> { - AudioMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map()) + AudioMessageType(type.content.body, type.content.source.map(), type.content.info?.map()) } is MessageType.File -> { - FileMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map()) + FileMessageType(type.content.body, type.content.source.map(), type.content.info?.map()) } is MessageType.Image -> { - ImageMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map()) + ImageMessageType(type.content.body, type.content.source.map(), type.content.info?.map()) } is MessageType.Notice -> { NoticeMessageType(type.content.body, type.content.formatted?.map()) @@ -60,7 +59,7 @@ class EventMessageMapper { EmoteMessageType(type.content.body, type.content.formatted?.map()) } is MessageType.Video -> { - VideoMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map()) + VideoMessageType(type.content.body, type.content.source.map(), type.content.info?.map()) } null -> { UnknownMessageType diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt index 51e84b441f..2a30d80323 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.matrix.impl.timeline.item.event import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange @@ -26,7 +27,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RedactedConte import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent -import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent import io.element.android.libraries.matrix.impl.media.map @@ -88,7 +88,7 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap StickerContent( body = kind.body, info = kind.info.map(), - url = kind.url + url = kind.url, ) } is TimelineItemContentKind.UnableToDecrypt -> { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt index fc14de03da..c8a383fadc 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt @@ -16,15 +16,16 @@ package io.element.android.libraries.matrix.test.media +import android.net.Uri import io.element.android.libraries.matrix.api.media.MatrixMediaLoader -import java.nio.file.Path -import kotlin.io.path.Path +import io.element.android.libraries.matrix.api.media.MatrixMediaSource +import java.io.File class FakeMediaLoader : MatrixMediaLoader { var shouldFail = false - override suspend fun loadMediaContent(url: String): Result { + override suspend fun loadMediaContent(source: MatrixMediaSource): Result { return if (shouldFail) { Result.failure(RuntimeException()) } else { @@ -32,7 +33,7 @@ class FakeMediaLoader : MatrixMediaLoader { } } - override suspend fun loadMediaThumbnail(url: String, width: Long, height: Long): Result { + override suspend fun loadMediaThumbnail(source: MatrixMediaSource, width: Long, height: Long): Result { return if (shouldFail) { Result.failure(RuntimeException()) } else { @@ -40,11 +41,11 @@ class FakeMediaLoader : MatrixMediaLoader { } } - override suspend fun loadMediaFile(url: String, mimeType: String?): Result { + override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result { return if (shouldFail) { Result.failure(RuntimeException()) } else { - return Result.success(Path("path")) + return Result.success(Uri.fromFile(File("path"))) } } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt index 37a7f02922..aa06e960c4 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt @@ -17,13 +17,12 @@ package io.element.android.libraries.matrix.ui.media import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.matrix.api.media.MatrixMediaSource import kotlin.math.roundToLong -fun AvatarData.toMediaRequestData(): MediaRequestData? { - return url?.let { - MediaRequestData( - url = it, - kind = MediaRequestData.Kind.Thumbnail(size.dp.value.roundToLong()) - ) - } +fun AvatarData.toMediaRequestData(): MediaRequestData { + return MediaRequestData( + source = url?.let { MatrixMediaSource(it) }, + kind = MediaRequestData.Kind.Thumbnail(size.dp.value.roundToLong()) + ) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt index 81b47c4022..a409b9de97 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt @@ -38,19 +38,15 @@ internal class CoilMediaFetcher( ByteBuffer.wrap(data) }.map { byteBuffer -> imageLoader.components.newFetcher(byteBuffer, options, imageLoader)?.first?.fetch() - } - .fold( - { result -> result }, - { failure -> throw failure } - ) + }.getOrThrow() } private suspend fun loadMedia(): Result { - if (mediaData == null) return Result.failure(IllegalStateException("No media data to fetch.")) + if (mediaData?.source == null) return Result.failure(IllegalStateException("No media data to fetch.")) return when (mediaData.kind) { - is MediaRequestData.Kind.Content -> mediaLoader.loadMediaContent(url = mediaData.url) + is MediaRequestData.Kind.Content -> mediaLoader.loadMediaContent(source = mediaData.source) is MediaRequestData.Kind.Thumbnail -> mediaLoader.loadMediaThumbnail( - url = mediaData.url, + source = mediaData.source, width = mediaData.kind.width, height = mediaData.kind.height ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt index f2f9bf9794..f3049ab4af 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt @@ -16,8 +16,10 @@ package io.element.android.libraries.matrix.ui.media +import io.element.android.libraries.matrix.api.media.MatrixMediaSource + data class MediaRequestData( - val url: String, + val source: MatrixMediaSource?, val kind: Kind ) { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt index 56ff15fd44..0064c1b63b 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestDataKeyer.kt @@ -22,7 +22,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData internal class AvatarDataKeyer : Keyer { override fun key(data: AvatarData, options: Options): String? { - return data.toMediaRequestData()?.toKey() + return data.toMediaRequestData().toKey() } } @@ -32,4 +32,7 @@ internal class MediaRequestDataKeyer : Keyer { } } -private fun MediaRequestData.toKey() = "${url}_${kind}" +private fun MediaRequestData.toKey(): String? { + if (source == null) return null + return "${source.url}_${kind}" +} From 2386ce733a6e231916479a49cab8dc0e8ed1fc12 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 5 May 2023 19:48:50 +0200 Subject: [PATCH 08/48] Introduce Attachments and LocalMedia (WIP) --- features/messages/impl/build.gradle.kts | 3 + .../messages/impl/MessagesFlowNode.kt | 37 ++++-- .../features/messages/impl/MessagesNode.kt | 8 ++ .../features/messages/impl/MessagesView.kt | 13 +- .../messages/impl/attachments/Attachment.kt | 29 +++++ .../preview/AttachmentsPreviewNode.kt | 53 ++++++++ .../preview/AttachmentsPreviewPresenter.kt | 42 +++++++ .../preview/AttachmentsPreviewState.kt | 23 ++++ .../AttachmentsPreviewStateProvider.kt | 36 ++++++ .../preview/AttachmentsPreviewView.kt | 66 ++++++++++ .../media/local/AndroidLocalMediaFactory.kt | 36 ++++++ .../impl/media/local/FakeLocalMediaFactory.kt | 29 +++++ .../LocalMedia.kt} | 27 ++--- .../LocalMediaFactory.kt} | 9 +- .../impl/media/local/LocalMediaView.kt | 113 ++++++++++++++++++ .../impl/media/viewer/MediaViewerNode.kt | 13 +- .../impl/media/viewer/MediaViewerPresenter.kt | 63 ++++++++++ .../impl/media/viewer/MediaViewerState.kt | 6 +- .../media/viewer/MediaViewerStateProvider.kt | 13 +- .../impl/media/viewer/MediaViewerView.kt | 80 ++++--------- .../textcomposer/MessageComposerEvents.kt | 2 + .../textcomposer/MessageComposerPresenter.kt | 39 +++--- .../impl/textcomposer/MessageComposerState.kt | 9 ++ .../MessageComposerStateProvider.kt | 1 + gradle/libs.versions.toml | 3 + 25 files changed, 634 insertions(+), 119 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/{viewer/model/MediaContentUiModel.kt => local/LocalMedia.kt} (54%) rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/{viewer/MediaViewerEvents.kt => local/LocalMediaFactory.kt} (74%) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 1b101b40f3..f9f32e6e0e 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -50,6 +50,9 @@ dependencies { implementation(libs.accompanist.flowlayout) implementation(libs.androidx.recyclerview) implementation(libs.jsoup) + implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.ui) + implementation(libs.accompanist.systemui) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 95a5d4ec9e..24122459a6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -26,18 +26,23 @@ import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.push +import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.messages.api.MessagesEntryPoint +import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode import io.element.android.features.messages.impl.media.viewer.MediaViewerNode -import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.media.MatrixMediaSource import kotlinx.android.parcel.Parcelize +import kotlinx.collections.immutable.ImmutableList @ContributesNode(RoomScope::class) class MessagesFlowNode @AssistedInject constructor( @@ -57,7 +62,10 @@ class MessagesFlowNode @AssistedInject constructor( object Messages : NavTarget @Parcelize - data class MediaViewer(val mediaContent: MediaContentUiModel) : NavTarget + data class MediaViewer(val title: String, val mediaSource: MatrixMediaSource) : NavTarget + + @Parcelize + data class AttachmentPreview(val attachment: Attachment) : NavTarget } private val callback = plugins().firstOrNull() @@ -73,25 +81,35 @@ class MessagesFlowNode @AssistedInject constructor( override fun onEventClicked(event: TimelineItem.Event) { processEventClicked(event) } + + override fun onPreviewAttachments(attachments: ImmutableList) { + backstack.push(NavTarget.AttachmentPreview(attachments.first())) + } } createNode(buildContext, listOf(callback)) } is NavTarget.MediaViewer -> { - val inputs = MediaViewerNode.Inputs(navTarget.mediaContent) + val inputs = MediaViewerNode.Inputs(navTarget.title, navTarget.mediaSource) createNode(buildContext, listOf(inputs)) } + is NavTarget.AttachmentPreview -> { + val inputs = AttachmentsPreviewNode.Inputs(navTarget.attachment) + createNode(buildContext, listOf(inputs)) + } } } private fun processEventClicked(event: TimelineItem.Event) { when (event.content) { is TimelineItemImageContent -> { - val mediaContent = MediaContentUiModel.Image( - body = event.content.body, - url = event.content.mediaRequestData.url, - blurhash = event.content.blurhash - ) - backstack.push(NavTarget.MediaViewer(mediaContent)) + val mediaSource = event.content.mediaSource + val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource) + backstack.push(navTarget) + } + is TimelineItemVideoContent -> { + val mediaSource = event.content.videoSource + val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource) + backstack.push(navTarget) } else -> Unit } @@ -102,6 +120,7 @@ class MessagesFlowNode @AssistedInject constructor( Children( navModel = backstack, modifier = modifier, + transitionHandler = rememberBackstackFader() ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index fb0804abd0..626b3bd682 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -25,8 +25,10 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.di.RoomScope +import kotlinx.collections.immutable.ImmutableList @ContributesNode(RoomScope::class) class MessagesNode @AssistedInject constructor( @@ -40,6 +42,7 @@ class MessagesNode @AssistedInject constructor( interface Callback : Plugin { fun onRoomDetailsClicked() fun onEventClicked(event: TimelineItem.Event) + fun onPreviewAttachments(attachments: ImmutableList) } private fun onRoomDetailsClicked() { @@ -50,6 +53,10 @@ class MessagesNode @AssistedInject constructor( callback?.onEventClicked(event) } + private fun onPreviewAttachments(attachments: ImmutableList) { + callback?.onPreviewAttachments(attachments) + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -58,6 +65,7 @@ class MessagesNode @AssistedInject constructor( onBackPressed = this::navigateUp, onRoomDetailsClicked = this::onRoomDetailsClicked, onEventClicked = this::onEventClicked, + onPreviewAttachments = this::onPreviewAttachments, modifier = modifier, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 8a14507004..b9cace4ded 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -62,7 +62,9 @@ import androidx.compose.ui.unit.sp import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.textcomposer.AttachmentSourcePicker +import io.element.android.features.messages.impl.textcomposer.AttachmentsState import io.element.android.features.messages.impl.textcomposer.MessageComposerEvents import io.element.android.features.messages.impl.textcomposer.MessageComposerView import io.element.android.features.messages.impl.timeline.TimelineView @@ -80,6 +82,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.LogCompositions +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch import timber.log.Timber @@ -89,6 +92,7 @@ fun MessagesView( onBackPressed: () -> Unit, onRoomDetailsClicked: () -> Unit, onEventClicked: (event: TimelineItem.Event) -> Unit, + onPreviewAttachments: (ImmutableList) -> Unit, modifier: Modifier = Modifier, ) { LogCompositions(tag = "MessagesScreen", msg = "Root") @@ -105,6 +109,13 @@ fun MessagesView( val bottomSheetState = rememberModalBottomSheetState(initialValue = initialBottomSheetState) val coroutineScope = rememberCoroutineScope() + val attachmentsState = state.composerState.attachmentsState + if (attachmentsState is AttachmentsState.Previewing) { + LaunchedEffect(attachmentsState) { + onPreviewAttachments(attachmentsState.attachments) + } + } + BackHandler(enabled = bottomSheetState.isVisible) { coroutineScope.launch { bottomSheetState.hide() @@ -327,5 +338,5 @@ internal fun MessagesViewDarkPreview(@PreviewParameter(MessagesStateProvider::cl @Composable private fun ContentToPreview(state: MessagesState) { - MessagesView(state, {}, {}, {}) + MessagesView(state, {}, {}, {}, {}) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt new file mode 100644 index 0000000000..53626a5037 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt @@ -0,0 +1,29 @@ +/* + * 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.attachments + +import android.os.Parcelable +import androidx.compose.runtime.Immutable +import io.element.android.features.messages.impl.media.local.LocalMedia +import kotlinx.parcelize.Parcelize + +@Immutable +sealed interface Attachment : Parcelable { + + @Parcelize + data class Media(val localMedia: LocalMedia) : Attachment +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt new file mode 100644 index 0000000000..2c96ece06b --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt @@ -0,0 +1,53 @@ +/* + * 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.attachments.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.RoomScope + +@ContributesNode(RoomScope::class) +class AttachmentsPreviewNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: AttachmentsPreviewPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs(val attachment: Attachment) : NodeInputs + + private val inputs: Inputs = inputs() + + private val presenter = presenterFactory.create(inputs.attachment) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + AttachmentsPreviewView( + state = state, + modifier = modifier + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt new file mode 100644 index 0000000000..6bce4a3bad --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.attachments.preview + +import androidx.compose.runtime.Composable +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.libraries.architecture.Presenter + +class AttachmentsPreviewPresenter @AssistedInject constructor( + @Assisted private val attachment: Attachment, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(attachment: Attachment): AttachmentsPreviewPresenter + } + + @Composable + override fun present(): AttachmentsPreviewState { + + return AttachmentsPreviewState( + attachment = attachment, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt new file mode 100644 index 0000000000..8e1b7fca78 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt @@ -0,0 +1,23 @@ +/* + * 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.attachments.preview + +import io.element.android.features.messages.impl.attachments.Attachment + +data class AttachmentsPreviewState( + val attachment: Attachment, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt new file mode 100644 index 0000000000..122a26ab5e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt @@ -0,0 +1,36 @@ +/* + * 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.attachments.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.core.net.toUri +import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.media.local.LocalMedia + +open class AttachmentsPreviewStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aAttachmentsPreviewState(), + // Add other states here + ) +} + +fun aAttachmentsPreviewState() = AttachmentsPreviewState( + attachment = Attachment.Media( + localMedia = LocalMedia("".toUri(), mimeType = null) + ) +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt new file mode 100644 index 0000000000..6b4449b620 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt @@ -0,0 +1,66 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package io.element.android.features.messages.impl.attachments.preview + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.media.local.LocalMediaView +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Scaffold + +@Composable +fun AttachmentsPreviewView( + state: AttachmentsPreviewState, + modifier: Modifier = Modifier, +) { + Scaffold(modifier) { + Box( + modifier = Modifier, + contentAlignment = Alignment.Center + ) { + when (state.attachment) { + is Attachment.Media -> LocalMediaView(localMedia = state.attachment.localMedia) + } + } + } +} + +@Preview +@Composable +fun AttachmentsPreviewViewLightPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun AttachmentsPreviewViewDarkPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: AttachmentsPreviewState) { + AttachmentsPreviewView( + state = state, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt new file mode 100644 index 0000000000..00136d616e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt @@ -0,0 +1,36 @@ +/* + * 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.local + +import android.content.Context +import android.net.Uri +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class AndroidLocalMediaFactory @Inject constructor( + @ApplicationContext private val context: Context +) : LocalMediaFactory { + + override fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia? { + if (uri == null) return null + val resolvedMimeType = mimeType ?: context.contentResolver.getType(uri) + return LocalMedia(uri, resolvedMimeType) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt new file mode 100644 index 0000000000..7f20492972 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt @@ -0,0 +1,29 @@ +/* + * 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.local + +import android.net.Uri + +class FakeLocalMediaFactory() : LocalMediaFactory { + + var mimeType: String? = null + + override fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia? { + if (uri == null) return null + return LocalMedia(uri, mimeType) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/model/MediaContentUiModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt similarity index 54% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/model/MediaContentUiModel.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt index 0f409364d2..1803bb3fb1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/model/MediaContentUiModel.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt @@ -14,27 +14,14 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.viewer.model +package io.element.android.features.messages.impl.media.local +import android.net.Uri import android.os.Parcelable -import io.element.android.libraries.matrix.ui.media.MediaRequestData import kotlinx.android.parcel.Parcelize -sealed interface MediaContentUiModel : Parcelable { - - @Parcelize - data class Image( - val body: String, - val url: String, - val blurhash: String?, - ) : MediaContentUiModel { - val mediaRequestData = MediaRequestData( - url = url, kind = MediaRequestData.Kind.Content - ) - } - - @Parcelize - data class Video( - val body: String, - ) : MediaContentUiModel -} +@Parcelize +data class LocalMedia( + val uri: Uri, + val mimeType: String?, +) : Parcelable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt similarity index 74% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt index 2f4398d570..08f026c4ac 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt @@ -14,9 +14,10 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.viewer +package io.element.android.features.messages.impl.media.local -// TODO Add your events or remove the file completely if no events -sealed interface MediaViewerEvents { - object MyEvent : MediaViewerEvents +import android.net.Uri + +interface LocalMediaFactory { + fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia? } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt new file mode 100644 index 0000000000..2cbe08543c --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt @@ -0,0 +1,113 @@ +/* + * 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.local + +import android.annotation.SuppressLint +import android.net.Uri +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.Lifecycle +import androidx.media3.common.MediaItem +import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.PlayerView +import coil.compose.AsyncImage +import io.element.android.libraries.designsystem.components.ZoomableBox +import io.element.android.libraries.designsystem.utils.OnLifecycleEvent + +@SuppressLint("UnsafeOptInUsageError") +@Composable +fun LocalMediaView( + localMedia: LocalMedia, + modifier: Modifier = Modifier +) { + when { + MimeTypes.isImage(localMedia.mimeType) -> MediaImageView( + uri = localMedia.uri, + modifier = modifier + ) + MimeTypes.isVideo(localMedia.mimeType) -> MediaVideoView( + uri = localMedia.uri, + modifier = modifier + ) + else -> Unit + } +} + +@Composable +private fun MediaImageView( + uri: Uri, + modifier: Modifier = Modifier, +) { + ZoomableBox( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + AsyncImage( + modifier = Modifier + .zoomable() + .fillMaxSize(), + model = uri, + contentDescription = "Image", + contentScale = ContentScale.Fit, + ) + } +} + +@UnstableApi +@Composable +fun MediaVideoView( + uri: Uri, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val exoPlayer = ExoPlayer.Builder(LocalContext.current).build() + val mediaItem = MediaItem.Builder() + .setUri(uri) + .build() + exoPlayer.playWhenReady + exoPlayer.setMediaItem(mediaItem) + exoPlayer.prepare() + + AndroidView( + factory = { + PlayerView(context).apply { + player = exoPlayer + resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT + layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + } + }, modifier = modifier.fillMaxSize() + ) + + OnLifecycleEvent { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> exoPlayer.play() + Lifecycle.Event.ON_PAUSE -> exoPlayer.pause() + Lifecycle.Event.ON_DESTROY -> exoPlayer.release() + else -> Unit + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt index 439d5c1ea1..02ec781815 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt @@ -24,25 +24,32 @@ import com.bumble.appyx.core.plugin.Plugin import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.media.MatrixMediaSource @ContributesNode(RoomScope::class) class MediaViewerNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + presenterFactory: MediaViewerPresenter.Factory, ) : Node(buildContext, plugins = plugins) { - data class Inputs(val mediaContent: MediaContentUiModel) : NodeInputs + data class Inputs( + val name: String, + val mediaSource: MatrixMediaSource, + ) : NodeInputs private val inputs: Inputs = inputs() + private val presenter = presenterFactory.create(inputs.name, inputs.mediaSource) + @Composable override fun View(modifier: Modifier) { + val state = presenter.present() MediaViewerView( - state = MediaViewerState(inputs.mediaContent), + state = state, modifier = modifier ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt new file mode 100644 index 0000000000..70408cc580 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt @@ -0,0 +1,63 @@ +/* + * 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 + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.features.messages.impl.media.local.LocalMedia +import io.element.android.features.messages.impl.media.local.LocalMediaFactory +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.media.MatrixMediaSource + +class MediaViewerPresenter @AssistedInject constructor( + @Assisted private val name: String, + @Assisted private val mediaSource: MatrixMediaSource, + private val localMediaFactory: LocalMediaFactory, + private val client: MatrixClient, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(name: String, mediaSource: MatrixMediaSource): MediaViewerPresenter + } + + @Composable + override fun present(): MediaViewerState { + val localMedia by produceState>(initialValue = Async.Uninitialized) { + value = Async.Loading(null) + //TODO we are missing some permissions to use this API + client.mediaLoader.loadMediaFile(mediaSource, null) + .onSuccess { + val localMedia = localMediaFactory.createFromUri(uri = it, null) + Async.Success(localMedia) + }.onFailure { + Async.Failure(it, null) + } + } + + return MediaViewerState( + name = name, + downloadedMedia = localMedia, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt index a8d02dea15..6a486b04fe 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt @@ -16,8 +16,10 @@ package io.element.android.features.messages.impl.media.viewer -import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel +import io.element.android.features.messages.impl.media.local.LocalMedia +import io.element.android.libraries.architecture.Async data class MediaViewerState( - val mediaContent: MediaContentUiModel + val name: String, + val downloadedMedia: Async ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt index 61cb4d36a1..19bc34535b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt @@ -17,22 +17,17 @@ package io.element.android.features.messages.impl.media.viewer import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel +import io.element.android.libraries.architecture.Async open class MediaViewerStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMediaViewerState(), + // Add other states here ) } fun aMediaViewerState() = MediaViewerState( - mediaContent = aMediaImage(), + name = "A media", + downloadedMedia = Async.Uninitialized ) - -private fun aMediaImage() = MediaContentUiModel.Image( - body = "a body", - url = "", - blurhash = null, -) - diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt index 38377ea05e..f3b42f1cc6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -14,84 +14,50 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3Api::class) + package io.element.android.features.messages.impl.media.viewer -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.awaitEachGesture -import androidx.compose.foundation.gestures.awaitFirstDown -import androidx.compose.foundation.gestures.calculatePan -import androidx.compose.foundation.gestures.calculateZoom import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable -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.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.features.messages.impl.media.viewer.model.MediaContentUiModel -import io.element.android.libraries.designsystem.components.ZoomableBox -import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage +import androidx.compose.ui.unit.dp +import io.element.android.features.messages.impl.media.local.LocalMediaView +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.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator +import io.element.android.libraries.designsystem.theme.components.Scaffold @Composable fun MediaViewerView( state: MediaViewerState, modifier: Modifier = Modifier, ) { - Box( - modifier = modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), - contentAlignment = Alignment.Center, - ) { - when (state.mediaContent) { - is MediaContentUiModel.Image -> MediaImageViewer(state.mediaContent) - is MediaContentUiModel.Video -> MediaVideoViewer(state.mediaContent) + Scaffold(modifier) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + when (state.downloadedMedia) { + is Async.Success -> LocalMediaView(state.downloadedMedia.state) + is Async.Failure -> ErrorDialog( + content = "Error while downloading the media", + ) + else -> CircularProgressIndicator( + strokeWidth = 2.dp, + ) + } } } } -@Composable -private fun MediaImageViewer( - image: MediaContentUiModel.Image, - modifier: Modifier = Modifier, -) { - ZoomableBox( - modifier = modifier.fillMaxSize(), - ) { - BlurHashAsyncImage( - blurHash = image.blurhash, - modifier = Modifier.fillMaxSize().zoomable(), - model = image.mediaRequestData, - contentScale = ContentScale.Fit, - ) - } -} - -@Composable -private fun MediaVideoViewer( - video: MediaContentUiModel.Video, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - - } -} - - @Preview @Composable fun MediaViewerViewLightPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt index b3728d1367..c89a08744a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt @@ -16,8 +16,10 @@ package io.element.android.features.messages.impl.textcomposer +import androidx.compose.runtime.Immutable import io.element.android.libraries.textcomposer.MessageComposerMode +@Immutable sealed interface MessageComposerEvents { object ToggleFullScreenState : MessageComposerEvents data class SendMessage(val message: String) : MessageComposerEvents diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt index 8d495aa269..d6133c5cc6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt @@ -16,6 +16,8 @@ package io.element.android.features.messages.impl.textcomposer +import android.annotation.SuppressLint +import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -25,6 +27,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.media3.common.MimeTypes +import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.media.local.LocalMediaFactory import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.core.data.toStableCharSequence @@ -35,9 +40,9 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.mediapickers.PickerProvider import io.element.android.libraries.textcomposer.MessageComposerMode +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @SingleIn(RoomScope::class) @@ -46,27 +51,32 @@ class MessageComposerPresenter @Inject constructor( private val room: MatrixRoom, private val mediaPickerProvider: PickerProvider, private val featureFlagService: FeatureFlagService, + private val localMediaFactory: LocalMediaFactory, ) : Presenter { + @SuppressLint("UnsafeOptInUsageError") @Composable override fun present(): MessageComposerState { val localCoroutineScope = rememberCoroutineScope() - val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker(onResult = { uri -> - Timber.d("Media picked from $uri") - }) + val attachmentsState = remember { + mutableStateOf(AttachmentsState.None) + } - val filesPicker = mediaPickerProvider.registerFilePicker(onResult = { uri -> - Timber.d("File picked from $uri") - }) + fun handlePickedMedia(uri: Uri?, mimeType: String? = null) { + val localMedia = localMediaFactory.createFromUri(uri, mimeType) + attachmentsState.value = if (localMedia == null) { + AttachmentsState.None + } else { + val mediaAttachment = Attachment.Media(localMedia) + AttachmentsState.Previewing(persistentListOf(mediaAttachment)) + } + } - val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { uri -> - Timber.d("Photo saved at $uri") - }) - - val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker(onResult = { uri -> - Timber.d("Video saved at $uri") - }) + val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker(onResult = { handlePickedMedia(it) }) + val filesPicker = mediaPickerProvider.registerFilePicker(onResult = { handlePickedMedia(it) }) + val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { handlePickedMedia(it, MimeTypes.IMAGE_JPEG) }, deleteAfter = false) + val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker(onResult = { handlePickedMedia(it, MimeTypes.VIDEO_MP4) }, deleteAfter = false) val isFullScreen = rememberSaveable { mutableStateOf(false) @@ -129,6 +139,7 @@ class MessageComposerPresenter @Inject constructor( isFullScreen = isFullScreen.value, mode = composerMode.value, attachmentSourcePicker = attachmentSourcePicker, + attachmentsState = attachmentsState.value, eventSink = ::handleEvents ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerState.kt index 7824f1f242..5129792313 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerState.kt @@ -17,8 +17,10 @@ package io.element.android.features.messages.impl.textcomposer import androidx.compose.runtime.Immutable +import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.textcomposer.MessageComposerMode +import kotlinx.collections.immutable.ImmutableList @Immutable data class MessageComposerState( @@ -26,11 +28,18 @@ data class MessageComposerState( val isFullScreen: Boolean, val mode: MessageComposerMode, val attachmentSourcePicker: AttachmentSourcePicker?, + val attachmentsState: AttachmentsState, val eventSink: (MessageComposerEvents) -> Unit ) { val isSendButtonVisible: Boolean = text?.charSequence.isNullOrEmpty().not() } +@Immutable +sealed interface AttachmentsState { + object None : AttachmentsState + data class Previewing(val attachments: ImmutableList) : AttachmentsState +} + sealed interface AttachmentSourcePicker { object AllMedia : AttachmentSourcePicker object Camera : AttachmentSourcePicker diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerStateProvider.kt index 38a88ee91f..af4dfd111d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerStateProvider.kt @@ -32,5 +32,6 @@ fun aMessageComposerState() = MessageComposerState( isFullScreen = false, mode = MessageComposerMode.Normal(content = ""), attachmentSourcePicker = null, + attachmentsState = AttachmentsState.None, eventSink = {} ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b4a9186cd5..885e480846 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ recyclerview = "1.3.0" lifecycle = "2.6.1" activity = "1.7.1" startup = "1.1.1" +media3 = "1.0.1" # Compose compose_bom = "2023.04.01" @@ -69,6 +70,8 @@ androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-kt androidx_lifecycle_process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" } androidx_splash = "androidx.core:core-splashscreen:1.0.1" androidx_security_crypto = "androidx.security:security-crypto:1.0.0" +androidx_media3_exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } +androidx_media3_ui = { module = "androidx.media3:media3-ui", version.ref = "media3" } androidx_activity_activity = { module = "androidx.activity:activity", version.ref = "activity" } androidx_activity_compose = { module = "androidx.activity:activity-compose", version.ref = "activity" } From 51a7a761b9434d883d39eb90aa5b9ca095baee27 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 5 May 2023 19:49:04 +0200 Subject: [PATCH 09/48] ZoomableBox small changes --- .../designsystem/components/ZoomableBox.kt | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ZoomableBox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ZoomableBox.kt index d9159da3c8..89ffd9a197 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ZoomableBox.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ZoomableBox.kt @@ -28,6 +28,7 @@ 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.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.RectangleShape @@ -39,27 +40,31 @@ import androidx.compose.ui.unit.IntSize @Composable fun ZoomableBox( modifier: Modifier = Modifier, - minScale: Float = 1f, - maxScale: Float = 5f, + contentAlignment: Alignment = Alignment.TopStart, + minZoom: Float = 1f, + maxZoom: Float = 5f, content: @Composable ZoomableBoxScope.() -> Unit ) { - var scale by remember { mutableStateOf(1f) } + var zoom by remember { mutableStateOf(minZoom) } var offsetX by remember { mutableStateOf(0f) } var offsetY by remember { mutableStateOf(0f) } var size by remember { mutableStateOf(IntSize.Zero) } + Box( modifier = modifier .clip(RectangleShape) - .onSizeChanged { size = it } + .onSizeChanged { + size = it + } .pointerInput(Unit) { - detectTransformGestures { _, pan, zoom, _ -> - scale = maxOf(minScale, minOf(scale * zoom, maxScale)) - val maxX = (size.width * (scale - 1)) / 2 + detectTransformGestures { _, panChange, zoomChange, _ -> + zoom = (zoom * zoomChange).coerceIn(minZoom, maxZoom) + val maxX = (size.width * (zoom - 1)) / 2f val minX = -maxX - offsetX = maxOf(minX, minOf(maxX, offsetX + pan.x)) - val maxY = (size.height * (scale - 1)) / 2 + val maxY = (size.height * (zoom - 1)) / 2f val minY = -maxY - offsetY = maxOf(minY, minOf(maxY, offsetY + pan.y)) + offsetX = maxOf(minX, minOf(maxX, offsetX + panChange.x)) + offsetY = maxOf(minY, minOf(maxY, offsetY + panChange.y)) } } .pointerInput(Unit) { @@ -67,17 +72,18 @@ fun ZoomableBox( onDoubleTap = { offsetX = 0f offsetY = 0f - scale = if (scale > minScale) { - minScale + zoom = if (zoom > minZoom) { + minZoom } else { - maxScale / 2f + maxZoom / 2f } } ) - } + }, + contentAlignment = contentAlignment, ) { - DefaultZoomableBoxScope(this, scale, offsetX, offsetY).content() + DefaultZoomableBoxScope(this, zoom, offsetX, offsetY).content() } } @@ -95,12 +101,12 @@ private class DefaultZoomableBoxScope( private val offsetY: Float ) : ZoomableBoxScope, BoxScope by parentScope { - override fun Modifier.zoomable(): Modifier { - return graphicsLayer( + override fun Modifier.zoomable() = this.then( + graphicsLayer( scaleX = scale, scaleY = scale, translationX = offsetX, - translationY = offsetY + translationY = offsetY, ) - } + ) } From 80adbd4bd1fac051ead6077110d61735f304468b Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 12 May 2023 18:48:24 +0200 Subject: [PATCH 10/48] Media: improve media viewer --- .../messages/impl/MessagesFlowNode.kt | 12 ++-- .../messages/impl/media/local/LocalMedia.kt | 2 +- .../impl/media/local/LocalMediaView.kt | 24 ++++--- .../impl/media/viewer/MediaViewerEvents.kt | 22 ++++++ .../impl/media/viewer/MediaViewerNode.kt | 3 +- .../impl/media/viewer/MediaViewerPresenter.kt | 68 ++++++++++++++----- .../impl/media/viewer/MediaViewerState.kt | 3 +- .../media/viewer/MediaViewerStateProvider.kt | 3 +- .../impl/media/viewer/MediaViewerView.kt | 44 ++++++++++-- .../TimelineItemContentMessageFactory.kt | 3 +- .../model/event/TimelineItemImageContent.kt | 1 + .../event/TimelineItemImageContentProvider.kt | 1 + .../model/event/TimelineItemVideoContent.kt | 2 +- .../event/TimelineItemVideoContentProvider.kt | 3 +- .../matrix/api/media/MatrixMediaLoader.kt | 6 +- .../libraries/matrix/api/media/MediaFile.kt | 27 ++++++++ .../matrix/impl/media/MediaHandle.kt | 31 +++++++++ .../matrix/impl/media/RustMediaLoader.kt | 14 ++-- .../matrix/test/media/FakeMediaFile.kt | 27 ++++++++ .../matrix/test/media/FakeMediaLoader.kt | 7 +- 20 files changed, 244 insertions(+), 59 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaFile.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaHandle.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaFile.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 24122459a6..a6b906d6fb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -62,7 +62,11 @@ class MessagesFlowNode @AssistedInject constructor( object Messages : NavTarget @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 data class AttachmentPreview(val attachment: Attachment) : NavTarget @@ -89,7 +93,7 @@ class MessagesFlowNode @AssistedInject constructor( createNode(buildContext, listOf(callback)) } is NavTarget.MediaViewer -> { - val inputs = MediaViewerNode.Inputs(navTarget.title, navTarget.mediaSource) + val inputs = MediaViewerNode.Inputs(navTarget.title, navTarget.mediaSource, navTarget.mimeType) createNode(buildContext, listOf(inputs)) } is NavTarget.AttachmentPreview -> { @@ -103,12 +107,12 @@ class MessagesFlowNode @AssistedInject constructor( when (event.content) { is TimelineItemImageContent -> { 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) } is TimelineItemVideoContent -> { 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) } else -> Unit diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt index 1803bb3fb1..a1ece7329a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt @@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.media.local import android.net.Uri import android.os.Parcelable -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize @Parcelize data class LocalMedia( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt index 2cbe08543c..7fc6ac5608 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt @@ -22,6 +22,8 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale @@ -84,13 +86,17 @@ fun MediaVideoView( modifier: Modifier = Modifier, ) { val context = LocalContext.current - val exoPlayer = ExoPlayer.Builder(LocalContext.current).build() - val mediaItem = MediaItem.Builder() - .setUri(uri) - .build() - exoPlayer.playWhenReady - exoPlayer.setMediaItem(mediaItem) - exoPlayer.prepare() + val exoPlayer = remember { + ExoPlayer.Builder(context).build() + .apply { + this.playWhenReady = true + this.prepare() + } + } + LaunchedEffect(uri) { + val mediaItem = MediaItem.fromUri(uri) + exoPlayer.setMediaItem(mediaItem) + } AndroidView( factory = { @@ -98,8 +104,10 @@ fun MediaVideoView( player = exoPlayer resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + controllerShowTimeoutMs = 3000 } - }, modifier = modifier.fillMaxSize() + }, + modifier = modifier.fillMaxSize() ) OnLifecycleEvent { _, event -> diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt new file mode 100644 index 0000000000..bfb346202c --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt @@ -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 +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt index 02ec781815..7f52e7e07a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt @@ -39,11 +39,12 @@ class MediaViewerNode @AssistedInject constructor( data class Inputs( val name: String, val mediaSource: MatrixMediaSource, + val mimeType: String? ) : NodeInputs private val inputs: Inputs = inputs() - private val presenter = presenterFactory.create(inputs.name, inputs.mediaSource) + private val presenter = presenterFactory.create(inputs) @Composable override fun View(modifier: Modifier) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt index 70408cc580..d727035176 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt @@ -17,8 +17,14 @@ package io.element.android.features.messages.impl.media.viewer import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState 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.AssistedFactory 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.Presenter 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( - @Assisted private val name: String, - @Assisted private val mediaSource: MatrixMediaSource, + @Assisted private val inputs: MediaViewerNode.Inputs, private val localMediaFactory: LocalMediaFactory, private val client: MatrixClient, ) : Presenter { @AssistedFactory interface Factory { - fun create(name: String, mediaSource: MatrixMediaSource): MediaViewerPresenter + fun create(inputs: MediaViewerNode.Inputs): MediaViewerPresenter } @Composable override fun present(): MediaViewerState { - val localMedia by produceState>(initialValue = Async.Uninitialized) { - value = Async.Loading(null) - //TODO we are missing some permissions to use this API - client.mediaLoader.loadMediaFile(mediaSource, null) - .onSuccess { - val localMedia = localMediaFactory.createFromUri(uri = it, null) - Async.Success(localMedia) - }.onFailure { - Async.Failure(it, null) - } + val coroutineScope = rememberCoroutineScope() + var loadMediaTrigger by remember { mutableStateOf(0) } + val mediaFile: MutableState = remember { + mutableStateOf(null) + } + val localMedia: MutableState> = remember { + mutableStateOf(Async.Uninitialized) + } + DisposableEffect(loadMediaTrigger) { + coroutineScope.loadMedia(mediaFile, localMedia) + onDispose { + mediaFile.value?.close() + } + } + + fun handleEvents(mediaViewerEvents: MediaViewerEvents) { + when (mediaViewerEvents) { + MediaViewerEvents.RetryLoading -> loadMediaTrigger++ + MediaViewerEvents.SaveOnDisk -> TODO() + } } return MediaViewerState( - name = name, - downloadedMedia = localMedia, + name = inputs.name, + downloadedMedia = localMedia.value, + eventSink = ::handleEvents ) } + + private fun CoroutineScope.loadMedia(mediaFile: MutableState, localMedia: MutableState>) = 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) + } + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt index 6a486b04fe..b5279af6c8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt @@ -21,5 +21,6 @@ import io.element.android.libraries.architecture.Async data class MediaViewerState( val name: String, - val downloadedMedia: Async + val downloadedMedia: Async, + val eventSink: (MediaViewerEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt index 19bc34535b..66c41a0eba 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt @@ -29,5 +29,6 @@ open class MediaViewerStateProvider : PreviewParameterProvider fun aMediaViewerState() = MediaViewerState( name = "A media", - downloadedMedia = Async.Uninitialized + downloadedMedia = Async.Uninitialized, + eventSink = {} ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt index f3b42f1cc6..a075656b78 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -19,37 +19,50 @@ package io.element.android.features.messages.impl.media.viewer 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.fillMaxWidth +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.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.media.local.LocalMediaView 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.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.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.R.string as StringR @Composable fun MediaViewerView( state: MediaViewerState, modifier: Modifier = Modifier, ) { + + fun onRetry() { + state.eventSink(MediaViewerEvents.RetryLoading) + } + Scaffold(modifier) { Box( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(it), contentAlignment = Alignment.Center ) { when (state.downloadedMedia) { is Async.Success -> LocalMediaView(state.downloadedMedia.state) - is Async.Failure -> ErrorDialog( - content = "Error while downloading the media", - ) + is Async.Failure -> ErrorView("Error while downloading", ::onRetry) else -> CircularProgressIndicator( 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 @Composable fun MediaViewerViewLightPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index 857049d1be..be751f4ece 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -52,6 +52,7 @@ class TimelineItemContentMessageFactory @Inject constructor() { body = messageType.body, height = messageType.info?.height?.toInt(), width = messageType.info?.width?.toInt(), + mimeType = messageType.info?.mimetype, mediaSource = messageType.source, blurhash = messageType.info?.blurhash, aspectRatio = aspectRatio @@ -69,7 +70,7 @@ class TimelineItemContentMessageFactory @Inject constructor() { body = messageType.body, thumbnailSource = messageType.info?.thumbnailSource, videoSource = messageType.source, - mimetype = messageType.info?.mimetype, + mimeType = messageType.info?.mimetype, width = messageType.info?.width?.toInt(), height = messageType.info?.height?.toInt(), duration = messageType.info?.duration ?: 0L, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt index 9f70d16de4..0d136a3144 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.media.MatrixMediaSource data class TimelineItemImageContent( val body: String, val mediaSource: MatrixMediaSource, + val mimeType: String?, val blurhash: String?, val width: Int?, val height: Int?, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt index a43551cd2d..0a3c339fdd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt @@ -34,6 +34,7 @@ fun aTimelineItemImageContent() = TimelineItemImageContent( mediaSource = MatrixMediaSource(""), blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr", aspectRatio = 0.5f, + mimeType = "null", height = null, width = null ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt index f45719a837..51a548e04c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt @@ -27,7 +27,7 @@ data class TimelineItemVideoContent( val blurhash: String?, val height: Int?, val width: Int?, - val mimetype: String?, + val mimeType: String?, ) : TimelineItemEventContent { override val type: String = "TimelineItemImageContent" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt index 7d9af16c79..4b4cc7240e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt @@ -18,7 +18,6 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.media.MatrixMediaSource -import io.element.android.libraries.matrix.ui.media.MediaRequestData open class TimelineItemVideoContentProvider : PreviewParameterProvider { override val values: Sequence @@ -38,5 +37,5 @@ fun aTimelineItemVideoContent() = TimelineItemVideoContent( videoSource = MatrixMediaSource(""), height = null, width = null, - mimetype = null + mimeType = null ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt index 43ab98c3b9..1fa7b3e592 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt @@ -16,8 +16,6 @@ package io.element.android.libraries.matrix.api.media -import android.net.Uri - interface MatrixMediaLoader { /** * @param url to fetch the content for. @@ -36,7 +34,7 @@ interface MatrixMediaLoader { /** * @param url to fetch the data for. * @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 + suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaFile.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaFile.kt new file mode 100644 index 0000000000..3ef659133d --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaFile.kt @@ -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 +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaHandle.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaHandle.kt new file mode 100644 index 0000000000..4b26b8c6c6 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaHandle.kt @@ -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() + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt index 52d4d7ccf4..87805f98bd 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt @@ -16,15 +16,14 @@ 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.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaSource +import io.element.android.libraries.matrix.api.media.MediaFile import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.mediaSourceFromUrl import org.matrix.rustcomponents.sdk.use -import java.io.File class RustMediaLoader( private val dispatchers: CoroutineDispatchers, @@ -59,19 +58,16 @@ class RustMediaLoader( } } - override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result = + override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result = withContext(dispatchers.io) { runCatching { mediaSourceFromUrl(source.url).use { mediaSource -> - innerClient.getMediaFile( + val mediaFile = innerClient.getMediaFile( mediaSource = mediaSource, mimeType = mimeType ?: "application/octet-stream" - ).use { - val file = File(it.path()) - Uri.fromFile(file) - } + ) + RustMediaFile(mediaFile) } } - } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaFile.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaFile.kt new file mode 100644 index 0000000000..275580d11e --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaFile.kt @@ -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 +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt index c8a383fadc..8369c309cc 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt @@ -16,10 +16,9 @@ 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.MatrixMediaSource -import java.io.File +import io.element.android.libraries.matrix.api.media.MediaFile class FakeMediaLoader : MatrixMediaLoader { @@ -41,11 +40,11 @@ class FakeMediaLoader : MatrixMediaLoader { } } - override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result { + override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result { return if (shouldFail) { Result.failure(RuntimeException()) } else { - return Result.success(Uri.fromFile(File("path"))) + return Result.success(FakeMediaFile("")) } } } From e3ad4ee06fa8cc6f3877e59121c8b3f5140d228c Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 16 May 2023 11:24:14 +0200 Subject: [PATCH 11/48] Media : extract an extension method to mediaupload --- .../textcomposer/MessageComposerPresenter.kt | 48 ++++++------------- .../mediaupload/api/MediaPreProcessor.kt | 3 ++ .../libraries/mediaupload/api/RoomExt.kt | 39 +++++++++++++++ .../mediaupload/MediaPreProcessorImpl.kt | 10 ++-- 4 files changed, 62 insertions(+), 38 deletions(-) create mode 100644 libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/RoomExt.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt index 9b8dcabdd2..54019b5e8a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt @@ -43,7 +43,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaType -import io.element.android.libraries.mediaupload.api.MediaUploadInfo +import io.element.android.libraries.mediaupload.api.sendMedia import io.element.android.libraries.textcomposer.MessageComposerMode import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope @@ -191,39 +191,21 @@ class MessageComposerPresenter @Inject constructor( mediaType: MediaType, deleteOriginal: Boolean = false ) = launch { - runCatching { - val info = handleMediaPreProcessing(uri, mediaType, deleteOriginal).getOrNull() ?: return@runCatching - when (info) { - is MediaUploadInfo.Image -> { - room.sendImage(info.file, info.thumbnailInfo.file, info.info) + mediaPreProcessor.process(uri, mediaType, deleteOriginal) + .map { info -> + room.sendMedia(info) + } + .onSuccess { + Timber.d("onSuccess sending media") + }.onFailure { failure -> + Timber.e(failure, "onfailure sending media: $failure") + val snackbarMessage = if (failure is MediaPreProcessor.Failure) { + StringR.string.screen_media_upload_preview_error_failed_processing + } else { + StringR.string.screen_media_upload_preview_error_failed_sending } + snackbarDispatcher.post(SnackbarMessage(snackbarMessage)) + } - is MediaUploadInfo.Video -> { - room.sendVideo(info.file, info.thumbnailInfo.file, info.info) - } - - is MediaUploadInfo.AnyFile -> { - room.sendFile(info.file, info.info) - } - else -> error("Unexpected MediaUploadInfo format: $info") - }.getOrThrow() - }.onFailure { - snackbarDispatcher.post(SnackbarMessage(StringR.string.screen_media_upload_preview_error_failed_sending)) - Timber.e(it, "Couldn't upload media") - }.onSuccess { - Timber.d("Media uploaded") - } - } - - private suspend fun handleMediaPreProcessing( - uri: Uri, - mediaType: MediaType, - deleteOriginal: Boolean, - ): Result { - val result = mediaPreProcessor.process(uri, mediaType, deleteOriginal = deleteOriginal) - Timber.d("Pre-processed media result: $result") - return result.onFailure { - snackbarDispatcher.post(SnackbarMessage(StringR.string.screen_media_upload_preview_error_failed_processing)) - } } } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt index 6e2168ca4b..cd45482871 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt @@ -29,4 +29,7 @@ interface MediaPreProcessor { mediaType: MediaType, deleteOriginal: Boolean = false ): Result + + data class Failure(override val cause: Throwable?) : RuntimeException(cause) } + diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/RoomExt.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/RoomExt.kt new file mode 100644 index 0000000000..ca5ec40ae8 --- /dev/null +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/RoomExt.kt @@ -0,0 +1,39 @@ +/* + * 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.mediaupload.api + +import io.element.android.libraries.matrix.api.room.MatrixRoom + +suspend fun MatrixRoom.sendMedia( + info: MediaUploadInfo, +): Result { + return when (info) { + is MediaUploadInfo.Image -> { + sendImage(info.file, info.thumbnailInfo.file, info.info) + } + + is MediaUploadInfo.Video -> { + sendVideo(info.file, info.thumbnailInfo.file, info.info) + } + + is MediaUploadInfo.AnyFile -> { + sendFile(info.file, info.info) + } + else -> error("Unexpected MediaUploadInfo format: $info") + } +} + diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/MediaPreProcessorImpl.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/MediaPreProcessorImpl.kt index 76cc7fae1e..6831cad565 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/MediaPreProcessorImpl.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/MediaPreProcessorImpl.kt @@ -25,6 +25,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.androidutils.file.createTmpFile import io.element.android.libraries.androidutils.media.runAndRelease import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage import io.element.android.libraries.di.AppScope @@ -71,6 +72,7 @@ class MediaPreProcessorImpl @Inject constructor( * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/?ref=blog.gitter.im#thumbnails). */ private const val THUMB_MAX_WIDTH = 800 + /** * Max height of thumbnail images. * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/?ref=blog.gitter.im#thumbnails). @@ -93,7 +95,7 @@ class MediaPreProcessorImpl @Inject constructor( // Camera returns an 'octet-stream' mimetype, so it needs to be overridden val mimeType = contentResolver.getType(uri) val mimeTypeOrDefault = if (mimeType == MimeTypes.OctetStream) { - when(mediaType) { + when (mediaType) { MediaType.Image -> MimeTypes.Jpeg MediaType.Video -> MimeTypes.Mp4 MediaType.Audio -> MimeTypes.Ogg @@ -104,7 +106,7 @@ class MediaPreProcessorImpl @Inject constructor( } val compressBeforeSending = mediaType in sequenceOf(MediaType.Image, MediaType.Video) val result = if (compressBeforeSending && mimeType != MimeTypes.Gif) { - when(mediaType) { + when (mediaType) { MediaType.Image -> processImage(uri) MediaType.Video -> processVideo(uri, mimeTypeOrDefault) MediaType.Audio -> processAudio(uri, mimeTypeOrDefault) @@ -124,13 +126,11 @@ class MediaPreProcessorImpl @Inject constructor( ) MediaUploadInfo.AnyFile(file, info) } - if (deleteOriginal) { contentResolver.delete(uri, null, null) } - result - } + }.mapFailure { MediaPreProcessor.Failure(it) } private suspend fun processImage(uri: Uri): MediaUploadInfo { val compressedFileResult = contentResolver.openInputStream(uri).use { input -> From 5176499195acb428d4baeb2140c29f5cd9dfbb51 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 16 May 2023 11:51:11 +0200 Subject: [PATCH 12/48] Media: fix encrypted media --- .../messages/impl/MessagesFlowNode.kt | 4 ++-- .../impl/media/viewer/MediaViewerNode.kt | 4 ++-- .../model/event/TimelineItemImageContent.kt | 4 ++-- .../event/TimelineItemImageContentProvider.kt | 5 ++-- .../model/event/TimelineItemVideoContent.kt | 6 ++--- .../event/TimelineItemVideoContentProvider.kt | 6 ++--- .../DefaultRoomLastMessageFormatterTests.kt | 10 ++++---- .../libraries/matrix/api/media/FileInfo.kt | 2 +- .../libraries/matrix/api/media/ImageInfo.kt | 2 +- .../matrix/api/media/MatrixMediaLoader.kt | 6 ++--- .../{MatrixMediaSource.kt => MediaSource.kt} | 11 +++++++-- .../libraries/matrix/api/media/VideoInfo.kt | 2 +- .../api/timeline/item/event/EventContent.kt | 10 ++++---- .../matrix/impl/media/MediaSource.kt | 6 +++-- .../matrix/impl/media/RustMediaLoader.kt | 24 +++++++++++++------ .../matrix/test/media/FakeMediaLoader.kt | 8 +++---- .../matrix/ui/media/AvatatarDataExt.kt | 4 ++-- .../matrix/ui/media/MediaRequestData.kt | 4 ++-- .../mediaupload/MediaPreProcessorImpl.kt | 6 ++--- .../mediaupload/test/FakeMediaPreProcessor.kt | 3 ++- 20 files changed, 73 insertions(+), 54 deletions(-) rename libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/{MatrixMediaSource.kt => MediaSource.kt} (80%) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index a6b906d6fb..c8e35ccbe8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -40,7 +40,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.media.MatrixMediaSource +import io.element.android.libraries.matrix.api.media.MediaSource import kotlinx.android.parcel.Parcelize import kotlinx.collections.immutable.ImmutableList @@ -64,7 +64,7 @@ class MessagesFlowNode @AssistedInject constructor( @Parcelize data class MediaViewer( val title: String, - val mediaSource: MatrixMediaSource, + val mediaSource: MediaSource, val mimeType: String? ) : NavTarget diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt index 7f52e7e07a..fe2167c786 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt @@ -27,7 +27,7 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.RoomScope -import io.element.android.libraries.matrix.api.media.MatrixMediaSource +import io.element.android.libraries.matrix.api.media.MediaSource @ContributesNode(RoomScope::class) class MediaViewerNode @AssistedInject constructor( @@ -38,7 +38,7 @@ class MediaViewerNode @AssistedInject constructor( data class Inputs( val name: String, - val mediaSource: MatrixMediaSource, + val mediaSource: MediaSource, val mimeType: String? ) : NodeInputs diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt index 0d136a3144..850dc9782c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt @@ -16,11 +16,11 @@ package io.element.android.features.messages.impl.timeline.model.event -import io.element.android.libraries.matrix.api.media.MatrixMediaSource +import io.element.android.libraries.matrix.api.media.MediaSource data class TimelineItemImageContent( val body: String, - val mediaSource: MatrixMediaSource, + val mediaSource: MediaSource, val mimeType: String?, val blurhash: String?, val width: Int?, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt index 0a3c339fdd..0bedfd8991 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt @@ -17,8 +17,7 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.matrix.api.media.MatrixMediaSource -import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.matrix.api.media.MediaSource open class TimelineItemImageContentProvider : PreviewParameterProvider { override val values: Sequence @@ -31,7 +30,7 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider { override val values: Sequence @@ -30,11 +30,11 @@ open class TimelineItemVideoContentProvider : PreviewParameterProvider + suspend fun loadMediaContent(source: MediaSource): Result /** * @param url to fetch the data for. @@ -29,12 +29,12 @@ interface MatrixMediaLoader { * @param height: the desired height for rescaling the media as thumbnail * @return a [Result] of ByteArray. It contains the binary data for the media. */ - suspend fun loadMediaThumbnail(source: MatrixMediaSource, width: Long, height: Long): Result + suspend fun loadMediaThumbnail(source: MediaSource, width: Long, height: Long): Result /** * @param url to fetch the data for. * @param mimeType: optional mime type * @return a [Result] of [MediaFile] */ - suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result + suspend fun loadMediaFile(source: MediaSource, mimeType: String?): Result } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaSource.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaSource.kt similarity index 80% rename from libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaSource.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaSource.kt index 22092c1afd..fe5217c2c2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaSource.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaSource.kt @@ -20,6 +20,13 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -data class MatrixMediaSource( - val url: String +data class MediaSource( + /** + * Url of the media + */ + val url: String, + /** + * This is used to hold data for encrypted media + */ + val json: String? = null, ) : Parcelable diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt index ca24a3303f..aa291bd653 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt @@ -23,6 +23,6 @@ data class VideoInfo( val mimetype: String?, val size: Long?, val thumbnailInfo: ThumbnailInfo?, - val thumbnailSource: MatrixMediaSource?, + val thumbnailSource: MediaSource?, val blurhash: String? ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt index 31bdfee4fa..dafaa95936 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventContent.kt @@ -21,7 +21,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo -import io.element.android.libraries.matrix.api.media.MatrixMediaSource +import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.VideoInfo sealed interface EventContent @@ -107,25 +107,25 @@ data class EmoteMessageType( data class ImageMessageType( val body: String, - val source: MatrixMediaSource, + val source: MediaSource, val info: ImageInfo? ) : MessageType data class AudioMessageType( val body: String, - val source: MatrixMediaSource, + val source: MediaSource, val info: AudioInfo? ) : MessageType data class VideoMessageType( val body: String, - val source: MatrixMediaSource, + val source: MediaSource, val info: VideoInfo? ) : MessageType data class FileMessageType( val body: String, - val source: MatrixMediaSource, + val source: MediaSource, val info: FileInfo? ) : MessageType diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaSource.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaSource.kt index 4d3dc6603f..c70bd0640f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaSource.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaSource.kt @@ -16,8 +16,10 @@ package io.element.android.libraries.matrix.impl.media -import io.element.android.libraries.matrix.api.media.MatrixMediaSource +import io.element.android.libraries.matrix.api.media.MediaSource import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource -fun RustMediaSource.map(): MatrixMediaSource = use { MatrixMediaSource(it.url()) } +fun RustMediaSource.map(): MediaSource = use { + MediaSource(it.url(), it.toJson()) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt index b7e69226c0..5e9c26d6c6 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt @@ -18,12 +18,13 @@ package io.element.android.libraries.matrix.impl.media 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.MatrixMediaSource import io.element.android.libraries.matrix.api.media.MediaFile +import io.element.android.libraries.matrix.api.media.MediaSource import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.mediaSourceFromUrl import org.matrix.rustcomponents.sdk.use +import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource class RustMediaLoader( private val dispatchers: CoroutineDispatchers, @@ -31,10 +32,10 @@ class RustMediaLoader( ) : MatrixMediaLoader { @OptIn(ExperimentalUnsignedTypes::class) - override suspend fun loadMediaContent(source: MatrixMediaSource): Result = + override suspend fun loadMediaContent(source: MediaSource): Result = withContext(dispatchers.io) { runCatching { - mediaSourceFromUrl(source.url).use { source -> + source.toRustMediaSource().use { source -> innerClient.getMediaContent(source).toUByteArray().toByteArray() } } @@ -42,13 +43,13 @@ class RustMediaLoader( @OptIn(ExperimentalUnsignedTypes::class) override suspend fun loadMediaThumbnail( - source: MatrixMediaSource, + source: MediaSource, width: Long, height: Long ): Result = withContext(dispatchers.io) { runCatching { - mediaSourceFromUrl(source.url).use { mediaSource -> + source.toRustMediaSource().use { mediaSource -> innerClient.getMediaThumbnail( mediaSource = mediaSource, width = width.toULong(), @@ -58,10 +59,10 @@ class RustMediaLoader( } } - override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result = + override suspend fun loadMediaFile(source: MediaSource, mimeType: String?): Result = withContext(dispatchers.io) { runCatching { - mediaSourceFromUrl(source.url).use { mediaSource -> + source.toRustMediaSource().use { mediaSource -> val mediaFile = innerClient.getMediaFile( mediaSource = mediaSource, body = null, @@ -71,4 +72,13 @@ class RustMediaLoader( } } } + + private fun MediaSource.toRustMediaSource(): RustMediaSource { + val json = this.json + return if (json != null) { + RustMediaSource.fromJson(json) + } else { + mediaSourceFromUrl(url) + } + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt index 8369c309cc..ebc823f6d1 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt @@ -17,14 +17,14 @@ package io.element.android.libraries.matrix.test.media 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.MediaSource import io.element.android.libraries.matrix.api.media.MediaFile class FakeMediaLoader : MatrixMediaLoader { var shouldFail = false - override suspend fun loadMediaContent(source: MatrixMediaSource): Result { + override suspend fun loadMediaContent(source: MediaSource): Result { return if (shouldFail) { Result.failure(RuntimeException()) } else { @@ -32,7 +32,7 @@ class FakeMediaLoader : MatrixMediaLoader { } } - override suspend fun loadMediaThumbnail(source: MatrixMediaSource, width: Long, height: Long): Result { + override suspend fun loadMediaThumbnail(source: MediaSource, width: Long, height: Long): Result { return if (shouldFail) { Result.failure(RuntimeException()) } else { @@ -40,7 +40,7 @@ class FakeMediaLoader : MatrixMediaLoader { } } - override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result { + override suspend fun loadMediaFile(source: MediaSource, mimeType: String?): Result { return if (shouldFail) { Result.failure(RuntimeException()) } else { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt index aa06e960c4..39912cb443 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/AvatatarDataExt.kt @@ -17,12 +17,12 @@ package io.element.android.libraries.matrix.ui.media import io.element.android.libraries.designsystem.components.avatar.AvatarData -import io.element.android.libraries.matrix.api.media.MatrixMediaSource +import io.element.android.libraries.matrix.api.media.MediaSource import kotlin.math.roundToLong fun AvatarData.toMediaRequestData(): MediaRequestData { return MediaRequestData( - source = url?.let { MatrixMediaSource(it) }, + source = url?.let { MediaSource(it) }, kind = MediaRequestData.Kind.Thumbnail(size.dp.value.roundToLong()) ) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt index f3049ab4af..02a7ed4e8c 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt @@ -16,10 +16,10 @@ package io.element.android.libraries.matrix.ui.media -import io.element.android.libraries.matrix.api.media.MatrixMediaSource +import io.element.android.libraries.matrix.api.media.MediaSource data class MediaRequestData( - val source: MatrixMediaSource?, + val source: MediaSource?, val kind: Kind ) { diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/MediaPreProcessorImpl.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/MediaPreProcessorImpl.kt index 6831cad565..799191251c 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/MediaPreProcessorImpl.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/MediaPreProcessorImpl.kt @@ -33,7 +33,7 @@ import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo -import io.element.android.libraries.matrix.api.media.MatrixMediaSource +import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.ThumbnailInfo import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.mediaupload.api.MediaPreProcessor @@ -217,7 +217,7 @@ class MediaPreProcessorImpl @Inject constructor( mimetype = mimeType, size = file.length(), thumbnailInfo = thumbnailInfo?.info, - thumbnailSource = thumbnailUrl?.let { MatrixMediaSource(it) }, + thumbnailSource = thumbnailUrl?.let { MediaSource(it) }, blurhash = thumbnailInfo?.blurhash, ) } @@ -251,7 +251,7 @@ fun ImageCompressionResult.toImageInfo(mimeType: String, thumbnailUrl: String?, mimetype = mimeType, size = size, thumbnailInfo = thumbnailInfo, - thumbnailSource = thumbnailUrl?.let { MatrixMediaSource(it) }, + thumbnailSource = thumbnailUrl?.let { MediaSource(it) }, blurhash = blurhash, ) diff --git a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt index 08a284af6c..e96bc08fd1 100644 --- a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt +++ b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt @@ -32,10 +32,11 @@ class FakeMediaPreProcessor : MediaPreProcessor { mimetype = "*/*", size = 999L, thumbnailInfo = null, - thumbnailUrl = null, + thumbnailSource = null, ) ) ) + override suspend fun process(uri: Uri, mediaType: MediaType, deleteOriginal: Boolean): Result = result fun givenResult(value: Result) { From 8c5e1c88ce9da2ec806bc8b25b795ee0850752d0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 16 May 2023 14:50:54 +0200 Subject: [PATCH 13/48] Media: update doc on MediaLoader --- .../android/libraries/matrix/api/media/MatrixMediaLoader.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt index 27272eea22..5dabef6d48 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt @@ -18,13 +18,13 @@ package io.element.android.libraries.matrix.api.media interface MatrixMediaLoader { /** - * @param url to fetch the content for. + * @param source to fetch the content for. * @return a [Result] of ByteArray. It contains the binary data for the media. */ suspend fun loadMediaContent(source: MediaSource): Result /** - * @param url to fetch the data for. + * @param source to fetch the data for. * @param width: the desired width for rescaling the media as thumbnail * @param height: the desired height for rescaling the media as thumbnail * @return a [Result] of ByteArray. It contains the binary data for the media. @@ -32,7 +32,7 @@ interface MatrixMediaLoader { suspend fun loadMediaThumbnail(source: MediaSource, width: Long, height: Long): Result /** - * @param url to fetch the data for. + * @param source to fetch the data for. * @param mimeType: optional mime type * @return a [Result] of [MediaFile] */ From c8ead4ab9fe53aa170dd48f8ef46a3a693636e28 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 16 May 2023 14:51:18 +0200 Subject: [PATCH 14/48] Media: update ui on media viewers --- .../preview/AttachmentsPreviewNode.kt | 12 ++-- .../preview/AttachmentsPreviewView.kt | 69 ++++++++++++++++--- .../impl/media/viewer/MediaViewerNode.kt | 11 +-- 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt index 2c96ece06b..33710d95d8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt @@ -27,6 +27,7 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.di.RoomScope @ContributesNode(RoomScope::class) @@ -45,9 +46,12 @@ class AttachmentsPreviewNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { val state = presenter.present() - AttachmentsPreviewView( - state = state, - modifier = modifier - ) + ElementTheme(darkTheme = true) { + AttachmentsPreviewView( + state = state, + onDismiss = this::navigateUp, + modifier = modifier + ) + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt index 6b4449b620..c69f19e019 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt @@ -18,40 +18,88 @@ package io.element.android.features.messages.impl.attachments.preview +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.media.local.LocalMediaView import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.ui.strings.R as StringsR @Composable fun AttachmentsPreviewView( state: AttachmentsPreviewState, + onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { + + fun onSendClicked() { + } + Scaffold(modifier) { - Box( - modifier = Modifier, - contentAlignment = Alignment.Center + Column( + modifier = Modifier.fillMaxWidth(), ) { - when (state.attachment) { - is Attachment.Media -> LocalMediaView(localMedia = state.attachment.localMedia) + Spacer( + modifier = Modifier.height(80.dp) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + when (state.attachment) { + is Attachment.Media -> LocalMediaView( + localMedia = state.attachment.localMedia + ) + } } + AttachmentsPreviewBottomActions( + onCancelClicked = onDismiss, + onSendClicked = ::onSendClicked, + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 120.dp) + .padding(all = 24.dp) + ) } } } -@Preview @Composable -fun AttachmentsPreviewViewLightPreview(@PreviewParameter(AttachmentsPreviewStateProvider::class) state: AttachmentsPreviewState) = - ElementPreviewLight { ContentToPreview(state) } +private fun AttachmentsPreviewBottomActions( + onCancelClicked: () -> Unit, + onSendClicked: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.SpaceBetween + ) { + TextButton(onClick = onCancelClicked) { + Text(stringResource(id = StringsR.string.action_cancel)) + } + TextButton(onClick = onSendClicked) { + Text(stringResource(id = StringsR.string.action_send)) + } + } +} @Preview @Composable @@ -62,5 +110,6 @@ fun AttachmentsPreviewViewDarkPreview(@PreviewParameter(AttachmentsPreviewStateP private fun ContentToPreview(state: AttachmentsPreviewState) { AttachmentsPreviewView( state = state, + onDismiss = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt index fe2167c786..ae82b222f7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt @@ -26,6 +26,7 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.media.MediaSource @@ -49,9 +50,11 @@ class MediaViewerNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { val state = presenter.present() - MediaViewerView( - state = state, - modifier = modifier - ) + ElementTheme(darkTheme = true) { + MediaViewerView( + state = state, + modifier = modifier + ) + } } } From f51d6a3cfdd7c449a855a1b54262d5b21878d62e Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 17 May 2023 08:44:35 +0200 Subject: [PATCH 15/48] Media : branch upload to preview screen (need improvement) --- .../preview/AttachmentsPreviewEvents.kt | 13 ++- .../preview/AttachmentsPreviewPresenter.kt | 63 ++++++++++ .../preview/AttachmentsPreviewState.kt | 3 + .../AttachmentsPreviewStateProvider.kt | 14 ++- .../preview/AttachmentsPreviewView.kt | 108 ++++++++++++++---- .../preview/error/ErrorFormatter.kt | 30 +++++ .../media/local/AndroidLocalMediaFactory.kt | 3 +- .../impl/media/local/FakeLocalMediaFactory.kt | 5 +- .../messages/impl/media/local/LocalMedia.kt | 2 +- .../impl/media/local/LocalMediaFactory.kt | 5 + .../textcomposer/MessageComposerPresenter.kt | 29 ----- .../libraries/core/extensions/Result.kt | 13 +++ .../mediaupload/api/MediaPreProcessor.kt | 4 +- ...sorImpl.kt => AndroidMediaPreProcessor.kt} | 34 +++--- .../mediaupload/test/FakeMediaPreProcessor.kt | 3 +- 15 files changed, 237 insertions(+), 92 deletions(-) rename libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaType.kt => features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvents.kt (68%) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/error/ErrorFormatter.kt rename libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/{MediaPreProcessorImpl.kt => AndroidMediaPreProcessor.kt} (90%) diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaType.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvents.kt similarity index 68% rename from libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaType.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvents.kt index e16ca43699..14a6a3fb2d 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaType.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewEvents.kt @@ -14,11 +14,12 @@ * limitations under the License. */ -package io.element.android.libraries.mediaupload.api +package io.element.android.features.messages.impl.attachments.preview -sealed interface MediaType { - object Image : MediaType - object Video : MediaType - object Audio : MediaType - object File : MediaType +import androidx.compose.runtime.Immutable + +@Immutable +sealed interface AttachmentsPreviewEvents { + object SendAttachment : AttachmentsPreviewEvents + object ClearSendState : AttachmentsPreviewEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt index 6bce4a3bad..6400b84725 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -16,15 +16,32 @@ package io.element.android.features.messages.impl.attachments.preview +import android.net.Uri import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.executeResult +import io.element.android.libraries.core.extensions.flatMap +import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.mediaupload.api.MediaPreProcessor +import io.element.android.libraries.mediaupload.api.sendMedia +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch class AttachmentsPreviewPresenter @AssistedInject constructor( @Assisted private val attachment: Attachment, + private val room: MatrixRoom, + private val mediaPreProcessor: MediaPreProcessor, + private val snackbarDispatcher: SnackbarDispatcher, ) : Presenter { @AssistedFactory @@ -35,8 +52,54 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( @Composable override fun present(): AttachmentsPreviewState { + val coroutineScope = rememberCoroutineScope() + + val sendActionState = remember { + mutableStateOf>(Async.Uninitialized) + } + + fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) { + when (attachmentsPreviewEvents) { + AttachmentsPreviewEvents.SendAttachment -> coroutineScope.sendAttachment(attachment, sendActionState) + AttachmentsPreviewEvents.ClearSendState -> sendActionState.value = Async.Uninitialized + } + } + return AttachmentsPreviewState( attachment = attachment, + sendActionState = sendActionState.value, + eventSink = ::handleEvents ) } + + private fun CoroutineScope.sendAttachment( + attachment: Attachment, + sendActionState: MutableState>, + ) = launch { + when (attachment) { + is Attachment.Media -> { + sendMedia( + uri = attachment.localMedia.uri, + mimeType = attachment.localMedia.mimeType, + deleteOriginal = false, + sendActionState = sendActionState + ) + } + } + } + + private suspend fun sendMedia( + uri: Uri, + mimeType: String, + deleteOriginal: Boolean = false, + sendActionState: MutableState>, + ) { + suspend { + mediaPreProcessor + .process(uri, mimeType, deleteOriginal) + .flatMap { info -> + room.sendMedia(info) + } + }.executeResult(sendActionState) + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt index 8e1b7fca78..67350f5048 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt @@ -17,7 +17,10 @@ package io.element.android.features.messages.impl.attachments.preview import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.libraries.architecture.Async data class AttachmentsPreviewState( val attachment: Attachment, + val sendActionState: Async, + val eventSink: (AttachmentsPreviewEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt index 122a26ab5e..151698b0f4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt @@ -20,17 +20,23 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.core.net.toUri import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.media.local.LocalMedia +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.core.mimetype.MimeTypes open class AttachmentsPreviewStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aAttachmentsPreviewState(), + anAttachmentsPreviewState(), + anAttachmentsPreviewState(sendActionState = Async.Loading()), + anAttachmentsPreviewState(sendActionState = Async.Failure(RuntimeException())), // Add other states here ) } -fun aAttachmentsPreviewState() = AttachmentsPreviewState( +fun anAttachmentsPreviewState(sendActionState: Async = Async.Uninitialized) = AttachmentsPreviewState( attachment = Attachment.Media( - localMedia = LocalMedia("".toUri(), mimeType = null) - ) + localMedia = LocalMedia("".toUri(), mimeType = MimeTypes.OctetStream), + ), + sendActionState = sendActionState, + eventSink = {} ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt index c69f19e019..8687eb85fa 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt @@ -29,17 +29,23 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.messages.impl.media.local.LocalMediaView +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.dialogs.RetryDialog import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.ui.strings.R import io.element.android.libraries.ui.strings.R as StringsR @Composable @@ -49,39 +55,93 @@ fun AttachmentsPreviewView( modifier: Modifier = Modifier, ) { - fun onSendClicked() { + fun postSendAttachment() { + state.eventSink(AttachmentsPreviewEvents.SendAttachment) + } + + fun postClearSendState() { + state.eventSink(AttachmentsPreviewEvents.ClearSendState) + } + + if (state.sendActionState is Async.Success) { + LaunchedEffect(state.sendActionState) { + onDismiss() + } } Scaffold(modifier) { - Column( - modifier = Modifier.fillMaxWidth(), - ) { - Spacer( - modifier = Modifier.height(80.dp) + Box { + AttachmentPreviewContent( + attachment = state.attachment, + onSendClicked = ::postSendAttachment, + onDismiss = onDismiss ) - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) { - when (state.attachment) { - is Attachment.Media -> LocalMediaView( - localMedia = state.attachment.localMedia - ) - } - } - AttachmentsPreviewBottomActions( - onCancelClicked = onDismiss, - onSendClicked = ::onSendClicked, - modifier = Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = 120.dp) - .padding(all = 24.dp) + AttachmentSendStateView( + sendActionState = state.sendActionState, + onRetryClicked = ::postSendAttachment, + onRetryDismissed = ::postClearSendState ) } } } +@Composable +private fun AttachmentSendStateView( + sendActionState: Async, + onRetryDismissed: () -> Unit, + onRetryClicked: () -> Unit +) { + when (sendActionState) { + is Async.Loading -> { + ProgressDialog(text = stringResource(id = R.string.common_loading)) + } + + is Async.Failure -> { + RetryDialog( + content = stringResource(sendAttachmentError(sendActionState.error)), + onDismiss = onRetryDismissed, + onRetry = onRetryClicked + ) + } + + else -> Unit + } +} + +@Composable +private fun AttachmentPreviewContent( + attachment: Attachment, + onSendClicked: () -> Unit, + onDismiss: () -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Spacer( + modifier = Modifier.height(80.dp) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + when (attachment) { + is Attachment.Media -> LocalMediaView( + localMedia = attachment.localMedia + ) + } + } + AttachmentsPreviewBottomActions( + onCancelClicked = onDismiss, + onSendClicked = onSendClicked, + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 120.dp) + .padding(all = 24.dp) + ) + } +} + @Composable private fun AttachmentsPreviewBottomActions( onCancelClicked: () -> Unit, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/error/ErrorFormatter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/error/ErrorFormatter.kt new file mode 100644 index 0000000000..92dcd21b8e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/error/ErrorFormatter.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.attachments.preview.error + +import io.element.android.libraries.mediaupload.api.MediaPreProcessor +import io.element.android.libraries.ui.strings.R + +fun sendAttachmentError( + throwable: Throwable +): Int { + return if (throwable is MediaPreProcessor.Failure) { + R.string.screen_media_upload_preview_error_failed_processing + } else { + R.string.screen_media_upload_preview_error_failed_sending + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt index 00136d616e..8c7bef7cbe 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.media.local import android.content.Context import android.net.Uri import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import javax.inject.Inject @@ -30,7 +31,7 @@ class AndroidLocalMediaFactory @Inject constructor( override fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia? { if (uri == null) return null - val resolvedMimeType = mimeType ?: context.contentResolver.getType(uri) + val resolvedMimeType = mimeType ?: context.contentResolver.getType(uri) ?: MimeTypes.OctetStream return LocalMedia(uri, resolvedMimeType) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt index 7f20492972..83915dd89d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt @@ -17,13 +17,14 @@ package io.element.android.features.messages.impl.media.local import android.net.Uri +import io.element.android.libraries.core.mimetype.MimeTypes class FakeLocalMediaFactory() : LocalMediaFactory { - var mimeType: String? = null + var fallbackMimeType: String = MimeTypes.OctetStream override fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia? { if (uri == null) return null - return LocalMedia(uri, mimeType) + return LocalMedia(uri, mimeType ?: fallbackMimeType) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt index a1ece7329a..c26a4baaff 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt @@ -23,5 +23,5 @@ import kotlinx.parcelize.Parcelize @Parcelize data class LocalMedia( val uri: Uri, - val mimeType: String?, + val mimeType: String, ) : Parcelable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt index 08f026c4ac..e7d2ea1ba6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt @@ -19,5 +19,10 @@ package io.element.android.features.messages.impl.media.local import android.net.Uri interface LocalMediaFactory { + /** + * This method will create a [LocalMedia] with the given [uri] and [mimeType] + * If the [mimeType] is null, it'll try to read it from the content. + * + */ fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia? } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt index 54019b5e8a..97d4ab25ca 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt @@ -34,7 +34,6 @@ import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.core.data.toStableCharSequence import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.utils.SnackbarDispatcher -import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.FeatureFlagService @@ -42,15 +41,11 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor -import io.element.android.libraries.mediaupload.api.MediaType -import io.element.android.libraries.mediaupload.api.sendMedia import io.element.android.libraries.textcomposer.MessageComposerMode import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject -import io.element.android.libraries.ui.strings.R as StringR @SingleIn(RoomScope::class) class MessageComposerPresenter @Inject constructor( @@ -59,8 +54,6 @@ class MessageComposerPresenter @Inject constructor( private val mediaPickerProvider: PickerProvider, private val featureFlagService: FeatureFlagService, private val localMediaFactory: LocalMediaFactory, - private val mediaPreProcessor: MediaPreProcessor, - private val snackbarDispatcher: SnackbarDispatcher, ) : Presenter { @SuppressLint("UnsafeOptInUsageError") @@ -186,26 +179,4 @@ class MessageComposerPresenter @Inject constructor( } } - private fun CoroutineScope.sendMedia( - uri: Uri, - mediaType: MediaType, - deleteOriginal: Boolean = false - ) = launch { - mediaPreProcessor.process(uri, mediaType, deleteOriginal) - .map { info -> - room.sendMedia(info) - } - .onSuccess { - Timber.d("onSuccess sending media") - }.onFailure { failure -> - Timber.e(failure, "onfailure sending media: $failure") - val snackbarMessage = if (failure is MediaPreProcessor.Failure) { - StringR.string.screen_media_upload_preview_error_failed_processing - } else { - StringR.string.screen_media_upload_preview_error_failed_sending - } - snackbarDispatcher.post(SnackbarMessage(snackbarMessage)) - } - - } } diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt index baa3b35e2b..3ddd4f9105 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt @@ -25,3 +25,16 @@ inline fun Result.mapFailure(transform: (exception: Throwable) -> else -> Result.failure(transform(exception)) } } + +/** + * Can be used to transform some Throwable into some other. + */ +inline fun Result.flatMap(transform: (R) -> Result): Result { + return when (val exception = exceptionOrNull()) { + null -> mapCatching(transform).fold( + onSuccess = { it }, + onFailure = { Result.failure(it) } + ) + else -> Result.failure(exception) + } +} diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt index cd45482871..2144f7bdd7 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt @@ -20,13 +20,13 @@ import android.net.Uri interface MediaPreProcessor { /** - * Given a [uri] and [mediaType], pre-processes the media before it's uploaded, resizing, transcoding, and removing sensitive info from its metadata. + * Given a [uri] and [mimeType], pre-processes the media before it's uploaded, resizing, transcoding, and removing sensitive info from its metadata. * If [deleteOriginal] is `true`, the file reference by the [uri] will be automatically deleted too when this process finishes. * @return a [Result] with the [MediaUploadInfo] containing all the info needed to begin the upload. */ suspend fun process( uri: Uri, - mediaType: MediaType, + mimeType: String, deleteOriginal: Boolean = false ): Result diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/MediaPreProcessorImpl.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt similarity index 90% rename from libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/MediaPreProcessorImpl.kt rename to libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt index 799191251c..3a500abbe6 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/MediaPreProcessorImpl.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt @@ -27,7 +27,9 @@ import io.element.android.libraries.androidutils.media.runAndRelease import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.media.AudioInfo @@ -37,7 +39,6 @@ import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.ThumbnailInfo import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.mediaupload.api.MediaPreProcessor -import io.element.android.libraries.mediaupload.api.MediaType import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo import kotlinx.coroutines.Dispatchers @@ -53,7 +54,7 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.seconds @ContributesBinding(AppScope::class) -class MediaPreProcessorImpl @Inject constructor( +class AndroidMediaPreProcessor @Inject constructor( @ApplicationContext private val context: Context, private val imageCompressor: ImageCompressor, private val videoCompressor: VideoCompressor, @@ -89,27 +90,18 @@ class MediaPreProcessorImpl @Inject constructor( override suspend fun process( uri: Uri, - mediaType: MediaType, + mimeType: String, deleteOriginal: Boolean, ): Result = runCatching { - // Camera returns an 'octet-stream' mimetype, so it needs to be overridden - val mimeType = contentResolver.getType(uri) - val mimeTypeOrDefault = if (mimeType == MimeTypes.OctetStream) { - when (mediaType) { - MediaType.Image -> MimeTypes.Jpeg - MediaType.Video -> MimeTypes.Mp4 - MediaType.Audio -> MimeTypes.Ogg - else -> mimeType - } - } else { - mimeType - } - val compressBeforeSending = mediaType in sequenceOf(MediaType.Image, MediaType.Video) - val result = if (compressBeforeSending && mimeType != MimeTypes.Gif) { - when (mediaType) { - MediaType.Image -> processImage(uri) - MediaType.Video -> processVideo(uri, mimeTypeOrDefault) - MediaType.Audio -> processAudio(uri, mimeTypeOrDefault) + val compressBeforeSending = ( + mimeType.isMimeTypeImage() && mimeType != MimeTypes.Gif) || + mimeType.isMimeTypeVideo() + + val result = if (compressBeforeSending) { + when { + mimeType.isMimeTypeImage() -> processImage(uri) + mimeType.isMimeTypeVideo() -> processVideo(uri, mimeType) + mimeType.isMimeTypeAudio() -> processAudio(uri, mimeType) else -> error("Cannot compress file of type: $mimeType") } } else { diff --git a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt index e96bc08fd1..f8b7237bbe 100644 --- a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt +++ b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt @@ -19,7 +19,6 @@ package io.element.android.libraries.mediaupload.test import android.net.Uri import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.mediaupload.api.MediaPreProcessor -import io.element.android.libraries.mediaupload.api.MediaType import io.element.android.libraries.mediaupload.api.MediaUploadInfo import java.io.File @@ -37,7 +36,7 @@ class FakeMediaPreProcessor : MediaPreProcessor { ) ) - override suspend fun process(uri: Uri, mediaType: MediaType, deleteOriginal: Boolean): Result = result + override suspend fun process(uri: Uri, mimeType: String, deleteOriginal: Boolean): Result = result fun givenResult(value: Result) { this.result = value From ad697bbe7b69c6433aa023d0c809b8afec15c13c Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 17 May 2023 16:19:18 +0200 Subject: [PATCH 16/48] Media: use blurhash library --- .../components/blurhash/BlurHashAsyncImage.kt | 6 +- .../components/blurhash/BlurHashDecoder.kt | 204 ------------------ 2 files changed, 4 insertions(+), 206 deletions(-) rename {libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem => features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline}/components/blurhash/BlurHashAsyncImage.kt (91%) delete mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashDecoder.kt diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashAsyncImage.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt similarity index 91% rename from libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashAsyncImage.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt index a7a432163c..3f6ca91e7f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashAsyncImage.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.designsystem.components.blurhash +package io.element.android.features.messages.impl.timeline.components.blurhash import android.graphics.Bitmap import androidx.compose.foundation.Image @@ -27,6 +27,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import coil.compose.SubcomposeAsyncImage +import com.vanniktech.blurhash.BlurHash @Composable fun BlurHashAsyncImage( @@ -58,12 +59,13 @@ fun BlurHashImage( contentDescription: String? = null, contentScale: ContentScale = ContentScale.Fit, ) { + if (blurHash == null) return val bitmapState = remember { mutableStateOf(null) } DisposableEffect(blurHash) { // Build a small blurhash image so that it's fast - bitmapState.value = BlurHashDecoder.decode(blurHash, 10, 10) + bitmapState.value = BlurHash.decode(blurHash, 10, 10) onDispose { bitmapState.value?.recycle() } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashDecoder.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashDecoder.kt deleted file mode 100644 index ec44a61ff3..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/blurhash/BlurHashDecoder.kt +++ /dev/null @@ -1,204 +0,0 @@ -/* - * 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.components.blurhash - -import android.graphics.Bitmap -import android.graphics.Color -import kotlin.math.cos -import kotlin.math.pow -import kotlin.math.withSign - -/** - * Extracted from https://github.com/woltapp/blurhash/blob/master/Kotlin/lib/src/main/java/com/wolt/blurhashkt/BlurHashDecoder.kt - */ -object BlurHashDecoder { - - // cache Math.cos() calculations to improve performance. - // The number of calculations can be huge for many bitmaps: width * height * numCompX * numCompY * 2 * nBitmaps - // the cache is enabled by default, it is recommended to disable it only when just a few images are displayed - private val cacheCosinesX = HashMap() - private val cacheCosinesY = HashMap() - - /** - * Clear calculations stored in memory cache. - * The cache is not big, but will increase when many image sizes are used, - * if the app needs memory it is recommended to clear it. - */ - fun clearCache() { - cacheCosinesX.clear() - cacheCosinesY.clear() - } - - /** - * Decode a blur hash into a new bitmap. - * - * @param useCache use in memory cache for the calculated math, reused by images with same size. - * if the cache does not exist yet it will be created and populated with new calculations. - * By default it is true. - */ - fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f, useCache: Boolean = true): Bitmap? { - if (blurHash == null || blurHash.length < 6) { - return null - } - val numCompEnc = decode83(blurHash, 0, 1) - val numCompX = (numCompEnc % 9) + 1 - val numCompY = (numCompEnc / 9) + 1 - if (blurHash.length != 4 + 2 * numCompX * numCompY) { - return null - } - val maxAcEnc = decode83(blurHash, 1, 2) - val maxAc = (maxAcEnc + 1) / 166f - val colors = Array(numCompX * numCompY) { i -> - if (i == 0) { - val colorEnc = decode83(blurHash, 2, 6) - decodeDc(colorEnc) - } else { - val from = 4 + i * 2 - val colorEnc = decode83(blurHash, from, from + 2) - decodeAc(colorEnc, maxAc * punch) - } - } - return composeBitmap(width, height, numCompX, numCompY, colors, useCache) - } - - private fun decode83(str: String, from: Int = 0, to: Int = str.length): Int { - var result = 0 - for (i in from until to) { - val index = charMap[str[i]] ?: -1 - if (index != -1) { - result = result * 83 + index - } - } - return result - } - - private fun decodeDc(colorEnc: Int): FloatArray { - val r = colorEnc shr 16 - val g = (colorEnc shr 8) and 255 - val b = colorEnc and 255 - return floatArrayOf(srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)) - } - - private fun srgbToLinear(colorEnc: Int): Float { - val v = colorEnc / 255f - return if (v <= 0.04045f) { - (v / 12.92f) - } else { - ((v + 0.055f) / 1.055f).pow(2.4f) - } - } - - private fun decodeAc(value: Int, maxAc: Float): FloatArray { - val r = value / (19 * 19) - val g = (value / 19) % 19 - val b = value % 19 - return floatArrayOf( - signedPow2((r - 9) / 9.0f) * maxAc, - signedPow2((g - 9) / 9.0f) * maxAc, - signedPow2((b - 9) / 9.0f) * maxAc - ) - } - - private fun signedPow2(value: Float) = value.pow(2f).withSign(value) - - private fun composeBitmap( - width: Int, height: Int, - numCompX: Int, numCompY: Int, - colors: Array, - useCache: Boolean - ): Bitmap { - // use an array for better performance when writing pixel colors - val imageArray = IntArray(width * height) - val calculateCosX = !useCache || !cacheCosinesX.containsKey(width * numCompX) - val cosinesX = getArrayForCosinesX(calculateCosX, width, numCompX) - val calculateCosY = !useCache || !cacheCosinesY.containsKey(height * numCompY) - val cosinesY = getArrayForCosinesY(calculateCosY, height, numCompY) - for (y in 0 until height) { - for (x in 0 until width) { - var r = 0f - var g = 0f - var b = 0f - for (j in 0 until numCompY) { - for (i in 0 until numCompX) { - val cosX = cosinesX.getCos(calculateCosX, i, numCompX, x, width) - val cosY = cosinesY.getCos(calculateCosY, j, numCompY, y, height) - val basis = (cosX * cosY).toFloat() - val color = colors[j * numCompX + i] - r += color[0] * basis - g += color[1] * basis - b += color[2] * basis - } - } - imageArray[x + width * y] = Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) - } - } - return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) - } - - private fun getArrayForCosinesY(calculate: Boolean, height: Int, numCompY: Int) = when { - calculate -> { - DoubleArray(height * numCompY).also { - cacheCosinesY[height * numCompY] = it - } - } - else -> { - cacheCosinesY[height * numCompY]!! - } - } - - private fun getArrayForCosinesX(calculate: Boolean, width: Int, numCompX: Int) = when { - calculate -> { - DoubleArray(width * numCompX).also { - cacheCosinesX[width * numCompX] = it - } - } - else -> cacheCosinesX[width * numCompX]!! - } - - private fun DoubleArray.getCos( - calculate: Boolean, - x: Int, - numComp: Int, - y: Int, - size: Int - ): Double { - if (calculate) { - this[x + numComp * y] = cos(Math.PI * y * x / size) - } - return this[x + numComp * y] - } - - private fun linearToSrgb(value: Float): Int { - val v = value.coerceIn(0f, 1f) - return if (v <= 0.0031308f) { - (v * 12.92f * 255f + 0.5f).toInt() - } else { - ((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt() - } - } - - private val charMap = listOf( - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', - 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', - 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', - 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',', - '-', '.', ':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~' - ) - .mapIndexed { i, c -> c to i } - .toMap() - -} From 6dedb43213992525433b4da35ec923851c53c0be Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 17 May 2023 16:20:12 +0200 Subject: [PATCH 17/48] Media: use telephoto library --- features/messages/impl/build.gradle.kts | 2 + .../impl/media/local/LocalMediaView.kt | 21 +--- gradle/libs.versions.toml | 2 + .../designsystem/components/ZoomableBox.kt | 112 ------------------ plugins/src/main/kotlin/Versions.kt | 2 +- 5 files changed, 11 insertions(+), 128 deletions(-) delete mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ZoomableBox.kt diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 1a54cb27c2..327746b5a2 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -54,6 +54,8 @@ dependencies { implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.ui) implementation(libs.accompanist.systemui) + implementation(libs.vanniktech.blurhash) + implementation(libs.telephoto.zoomableimage) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt index 7fc6ac5608..b5591c14f2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext @@ -36,9 +35,8 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView -import coil.compose.AsyncImage -import io.element.android.libraries.designsystem.components.ZoomableBox import io.element.android.libraries.designsystem.utils.OnLifecycleEvent +import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage @SuppressLint("UnsafeOptInUsageError") @Composable @@ -64,19 +62,12 @@ private fun MediaImageView( uri: Uri, modifier: Modifier = Modifier, ) { - ZoomableBox( + ZoomableAsyncImage( modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - AsyncImage( - modifier = Modifier - .zoomable() - .fillMaxSize(), - model = uri, - contentDescription = "Image", - contentScale = ContentScale.Fit, - ) - } + model = uri, + contentDescription = "Image", + contentScale = ContentScale.Fit, + ) } @UnstableApi diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d3c8167441..a917c8ddbe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,6 +42,7 @@ appyx = "1.2.0" dependencycheck = "8.2.1" stem = "2.3.0" sqldelight = "1.5.5" +telephoto = "0.3.0" # DI dagger = "2.46.1" @@ -142,6 +143,7 @@ unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1" gujun_span = "me.gujun.android:span:1.7" otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5" vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0" +telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" } # Di inject = "javax.inject:javax.inject:1" diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ZoomableBox.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ZoomableBox.kt deleted file mode 100644 index 89ffd9a197..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ZoomableBox.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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.components - -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.gestures.detectTransformGestures -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.LayoutScopeMarker -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -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.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.unit.IntSize - -@Composable -fun ZoomableBox( - modifier: Modifier = Modifier, - contentAlignment: Alignment = Alignment.TopStart, - minZoom: Float = 1f, - maxZoom: Float = 5f, - content: @Composable ZoomableBoxScope.() -> Unit -) { - var zoom by remember { mutableStateOf(minZoom) } - var offsetX by remember { mutableStateOf(0f) } - var offsetY by remember { mutableStateOf(0f) } - var size by remember { mutableStateOf(IntSize.Zero) } - - Box( - modifier = modifier - .clip(RectangleShape) - .onSizeChanged { - size = it - } - .pointerInput(Unit) { - detectTransformGestures { _, panChange, zoomChange, _ -> - zoom = (zoom * zoomChange).coerceIn(minZoom, maxZoom) - val maxX = (size.width * (zoom - 1)) / 2f - val minX = -maxX - val maxY = (size.height * (zoom - 1)) / 2f - val minY = -maxY - offsetX = maxOf(minX, minOf(maxX, offsetX + panChange.x)) - offsetY = maxOf(minY, minOf(maxY, offsetY + panChange.y)) - } - } - .pointerInput(Unit) { - detectTapGestures( - onDoubleTap = { - offsetX = 0f - offsetY = 0f - zoom = if (zoom > minZoom) { - minZoom - } else { - maxZoom / 2f - } - - } - ) - }, - contentAlignment = contentAlignment, - ) { - DefaultZoomableBoxScope(this, zoom, offsetX, offsetY).content() - } -} - -@LayoutScopeMarker -@Immutable -interface ZoomableBoxScope : BoxScope { - @Stable - fun Modifier.zoomable(): Modifier -} - -private class DefaultZoomableBoxScope( - private val parentScope: BoxScope, - private val scale: Float, - private val offsetX: Float, - private val offsetY: Float -) : ZoomableBoxScope, BoxScope by parentScope { - - override fun Modifier.zoomable() = this.then( - graphicsLayer( - scaleX = scale, - scaleY = scale, - translationX = offsetX, - translationY = offsetY, - ) - ) -} diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 36a3a33e70..d7c60d7fb0 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -23,7 +23,7 @@ object Versions { const val compileSdk = 33 const val targetSdk = 33 - const val minSdk = 23 + const val minSdk = 24 val javaCompileVersion = JavaVersion.VERSION_17 val javaLanguageVersion: JavaLanguageVersion = JavaLanguageVersion.of(11) } From 129ad0be092e22dd12d3c71d6f34afc6be863982 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 17 May 2023 16:20:31 +0200 Subject: [PATCH 18/48] Media: some ui improvements --- .../preview/AttachmentsPreviewNode.kt | 2 +- .../preview/AttachmentsPreviewView.kt | 16 +++---- .../impl/media/viewer/MediaViewerNode.kt | 2 +- .../impl/media/viewer/MediaViewerView.kt | 8 ++-- .../event/TimelineItemAspectRatioBox.kt | 42 +++++++++++++++++++ .../components/event/TimelineItemImageView.kt | 18 +++----- .../components/event/TimelineItemVideoView.kt | 21 ++++------ .../TimelineItemContentMessageFactory.kt | 24 +++++------ 8 files changed, 81 insertions(+), 52 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt index 33710d95d8..a31368896e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt @@ -45,8 +45,8 @@ class AttachmentsPreviewNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - val state = presenter.present() ElementTheme(darkTheme = true) { + val state = presenter.present() AttachmentsPreviewView( state = state, onDismiss = this::navigateUp, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt index 8687eb85fa..3ff750f101 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt @@ -22,14 +22,14 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -70,7 +70,10 @@ fun AttachmentsPreviewView( } Scaffold(modifier) { - Box { + Box( + modifier = Modifier.padding(it), + contentAlignment = Alignment.Center + ) { AttachmentPreviewContent( attachment = state.attachment, onSendClicked = ::postSendAttachment, @@ -115,11 +118,10 @@ private fun AttachmentPreviewContent( onDismiss: () -> Unit ) { Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxSize() + .padding(top = 24.dp) ) { - Spacer( - modifier = Modifier.height(80.dp) - ) Box( modifier = Modifier .fillMaxWidth() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt index ae82b222f7..2ebdba3b1c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt @@ -49,8 +49,8 @@ class MediaViewerNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - val state = presenter.present() ElementTheme(darkTheme = true) { + val state = presenter.present() MediaViewerView( state = state, modifier = modifier diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt index a075656b78..9dbcaf5559 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -41,7 +41,7 @@ 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.Scaffold import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.ui.strings.R.string as StringR +import io.element.android.libraries.ui.strings.R as StringR @Composable fun MediaViewerView( @@ -62,7 +62,7 @@ fun MediaViewerView( ) { when (state.downloadedMedia) { is Async.Success -> LocalMediaView(state.downloadedMedia.state) - is Async.Failure -> ErrorView("Error while downloading", ::onRetry) + is Async.Failure -> ErrorView(stringResource(id = StringR.string.error_unknown), ::onRetry) else -> CircularProgressIndicator( strokeWidth = 2.dp, ) @@ -82,11 +82,11 @@ private fun ErrorView( horizontalAlignment = Alignment.CenterHorizontally, ) { Text(text = errorMessage) - Spacer(modifier = Modifier.size(8.dp)) + Spacer(modifier = Modifier.size(16.dp)) Button( onClick = onRetry ) { - Text(text = stringResource(id = StringR.action_retry)) + Text(text = stringResource(id = StringR.string.action_retry)) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt new file mode 100644 index 0000000000..c3618bb3d2 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components.event + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.heightIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlin.math.min + +@Composable +fun TimelineItemAspectRatioBox( + height: Int?, + aspectRatio: Float, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + val maxHeight = min(300, height ?: 0) + Box( + modifier = modifier + .heightIn(max = maxHeight.dp) + .aspectRatio(aspectRatio), + content = content + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index 7fa2076574..f1dd1625f0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -16,37 +16,31 @@ package io.element.android.features.messages.impl.timeline.components.event -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp +import io.element.android.features.messages.impl.timeline.components.blurhash.BlurHashAsyncImage import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider -import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.matrix.ui.media.MediaRequestData -import kotlin.math.min @Composable fun TimelineItemImageView( content: TimelineItemImageContent, modifier: Modifier = Modifier, ) { - val maxHeight = min(300, content.height ?: Int.MAX_VALUE) - Box( + TimelineItemAspectRatioBox( + height = content.height, + aspectRatio = content.aspectRatio, modifier = modifier - .heightIn(max = maxHeight.dp) - .aspectRatio(content.aspectRatio), - contentAlignment = Alignment.Center, ) { BlurHashAsyncImage( + modifier = Modifier.fillMaxSize(), blurHash = content.blurhash, model = MediaRequestData(content.mediaSource, MediaRequestData.Kind.Content), contentScale = ContentScale.Fit, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index 5dec3717c8..c56c9b18fb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -17,9 +17,7 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -29,29 +27,26 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp +import io.element.android.features.messages.impl.timeline.components.blurhash.BlurHashAsyncImage import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent -import io.element.android.libraries.designsystem.components.blurhash.BlurHashAsyncImage import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.matrix.ui.media.MediaRequestData -import kotlin.math.min @Composable fun TimelineItemVideoView( content: TimelineItemVideoContent, modifier: Modifier = Modifier, ) { - val maxHeight = min(300, content.height ?: Int.MAX_VALUE) - Box( + TimelineItemAspectRatioBox( + height = content.height, + aspectRatio = content.aspectRatio, modifier = modifier - .heightIn(max = maxHeight.dp) - .aspectRatio(content.aspectRatio), - contentAlignment = Alignment.Center, ) { BlurHashAsyncImage( + modifier = Modifier.fillMaxSize(), blurHash = content.blurhash, model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.Content), contentScale = ContentScale.Fit, @@ -59,9 +54,9 @@ fun TimelineItemVideoView( Image( painterResource(id = androidx.media3.ui.R.drawable.exo_ic_play_circle_filled), contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground) + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground), + modifier = Modifier.align(Alignment.Center), ) - } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index be751f4ece..4c5fa78a06 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -41,13 +41,7 @@ class TimelineItemContentMessageFactory @Inject constructor() { htmlDocument = messageType.formatted?.toHtmlDocument() ) is ImageMessageType -> { - val height = messageType.info?.height?.toFloat() - val width = messageType.info?.width?.toFloat() - val aspectRatio = if (height != null && width != null) { - width / height - } else { - 0.7f - } + val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) TimelineItemImageContent( body = messageType.body, height = messageType.info?.height?.toInt(), @@ -59,13 +53,7 @@ class TimelineItemContentMessageFactory @Inject constructor() { ) } is VideoMessageType -> { - val height = messageType.info?.height?.toFloat() - val width = messageType.info?.width?.toFloat() - val aspectRatio = if (height != null && width != null) { - width / height - } else { - 0.7f - } + val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) TimelineItemVideoContent( body = messageType.body, thumbnailSource = messageType.info?.thumbnailSource, @@ -89,4 +77,12 @@ class TimelineItemContentMessageFactory @Inject constructor() { else -> TimelineItemUnknownContent } } + + private fun aspectRatioOf(width: Long?, height: Long?): Float { + return if (height != null && width != null) { + width.toFloat() / height.toFloat() + } else { + 0.7f + } + } } From 62b66b2111fe51176dcd604fe4aeeece0be1ea4d Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 17 May 2023 17:48:18 +0200 Subject: [PATCH 19/48] Media : some improvements and cleaning --- .../messages/impl/MessagesPresenter.kt | 6 +- .../features/messages/impl/MessagesState.kt | 2 +- .../messages/impl/MessagesStateProvider.kt | 4 +- .../features/messages/impl/MessagesView.kt | 8 +-- .../preview/AttachmentsPreviewPresenter.kt | 18 +----- .../impl/media/viewer/MediaViewerEvents.kt | 1 - .../impl/media/viewer/MediaViewerPresenter.kt | 1 - .../MessageComposerEvents.kt | 2 +- .../MessageComposerPresenter.kt | 4 +- .../MessageComposerState.kt | 2 +- .../MessageComposerStateProvider.kt | 2 +- .../MessageComposerView.kt | 2 +- .../components/blurhash/BlurHashAsyncImage.kt | 2 +- .../messages/MessagesPresenterTest.kt | 2 +- .../MessageComposerPresenterTest.kt | 8 +-- .../matrix/impl/room/RustMatrixRoom.kt | 17 +++--- .../libraries/mediaupload/api/MediaSender.kt | 55 +++++++++++++++++++ .../libraries/mediaupload/api/RoomExt.kt | 39 ------------- .../mediaupload/AndroidMediaPreProcessor.kt | 4 +- 19 files changed, 90 insertions(+), 89 deletions(-) rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/{textcomposer => messagecomposer}/MessageComposerEvents.kt (96%) rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/{textcomposer => messagecomposer}/MessageComposerPresenter.kt (97%) rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/{textcomposer => messagecomposer}/MessageComposerState.kt (96%) rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/{textcomposer => messagecomposer}/MessageComposerStateProvider.kt (95%) rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/{textcomposer => messagecomposer}/MessageComposerView.kt (97%) create mode 100644 libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt delete mode 100644 libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/RoomExt.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 13f36c2ab6..c075d4321e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -27,9 +27,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction -import io.element.android.features.messages.impl.textcomposer.MessageComposerEvents -import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter -import io.element.android.features.messages.impl.textcomposer.MessageComposerState +import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents +import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter +import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.model.TimelineItem diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 34e5f664ea..8c876ea49c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -18,7 +18,7 @@ package io.element.android.features.messages.impl import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.actionlist.ActionListState -import io.element.android.features.messages.impl.textcomposer.MessageComposerState +import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.timeline.TimelineState import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.utils.SnackbarMessage diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 9249b808a1..d235232560 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -18,8 +18,8 @@ package io.element.android.features.messages.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.actionlist.anActionListState -import io.element.android.features.messages.impl.textcomposer.AttachmentSourcePicker -import io.element.android.features.messages.impl.textcomposer.aMessageComposerState +import io.element.android.features.messages.impl.messagecomposer.AttachmentSourcePicker +import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.timeline.aTimelineItemContent import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.aTimelineState diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index b571c98031..9e5c974949 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -64,10 +64,10 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.attachments.Attachment -import io.element.android.features.messages.impl.textcomposer.AttachmentSourcePicker -import io.element.android.features.messages.impl.textcomposer.AttachmentsState -import io.element.android.features.messages.impl.textcomposer.MessageComposerEvents -import io.element.android.features.messages.impl.textcomposer.MessageComposerView +import io.element.android.features.messages.impl.messagecomposer.AttachmentSourcePicker +import io.element.android.features.messages.impl.messagecomposer.AttachmentsState +import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents +import io.element.android.features.messages.impl.messagecomposer.MessageComposerView import io.element.android.features.messages.impl.timeline.TimelineView import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt index 6400b84725..edc0248d18 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -29,19 +29,13 @@ import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.executeResult -import io.element.android.libraries.core.extensions.flatMap -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.mediaupload.api.MediaPreProcessor -import io.element.android.libraries.mediaupload.api.sendMedia +import io.element.android.libraries.mediaupload.api.MediaSender import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch class AttachmentsPreviewPresenter @AssistedInject constructor( @Assisted private val attachment: Attachment, - private val room: MatrixRoom, - private val mediaPreProcessor: MediaPreProcessor, - private val snackbarDispatcher: SnackbarDispatcher, + private val mediaSender: MediaSender, ) : Presenter { @AssistedFactory @@ -81,7 +75,6 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( sendMedia( uri = attachment.localMedia.uri, mimeType = attachment.localMedia.mimeType, - deleteOriginal = false, sendActionState = sendActionState ) } @@ -91,15 +84,10 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( private suspend fun sendMedia( uri: Uri, mimeType: String, - deleteOriginal: Boolean = false, sendActionState: MutableState>, ) { suspend { - mediaPreProcessor - .process(uri, mimeType, deleteOriginal) - .flatMap { info -> - room.sendMedia(info) - } + mediaSender.sendMedia(uri, mimeType) }.executeResult(sendActionState) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt index bfb346202c..4b60c05b3d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt @@ -18,5 +18,4 @@ package io.element.android.features.messages.impl.media.viewer sealed interface MediaViewerEvents { object RetryLoading : MediaViewerEvents - object SaveOnDisk : MediaViewerEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt index d727035176..7f672fcd3e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt @@ -68,7 +68,6 @@ class MediaViewerPresenter @AssistedInject constructor( fun handleEvents(mediaViewerEvents: MediaViewerEvents) { when (mediaViewerEvents) { MediaViewerEvents.RetryLoading -> loadMediaTrigger++ - MediaViewerEvents.SaveOnDisk -> TODO() } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt similarity index 96% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt index c89a08744a..92ce191ce1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.textcomposer +package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.Immutable import io.element.android.libraries.textcomposer.MessageComposerMode diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt similarity index 97% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 97d4ab25ca..527d875f25 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.textcomposer +package io.element.android.features.messages.impl.messagecomposer import android.annotation.SuppressLint import android.net.Uri @@ -33,14 +33,12 @@ import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.core.data.toStableCharSequence import io.element.android.libraries.core.mimetype.MimeTypes -import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.mediapickers.api.PickerProvider -import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.textcomposer.MessageComposerMode import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt similarity index 96% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerState.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index 5129792313..9df3e2269a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.textcomposer +package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.attachments.Attachment diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt similarity index 95% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerStateProvider.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index af4dfd111d..b55a168814 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.textcomposer +package io.element.android.features.messages.impl.messagecomposer import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.core.data.StableCharSequence diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt similarity index 97% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerView.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index 9b167013da..63fe656cd2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.textcomposer +package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt index 3f6ca91e7f..7f65a54795 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt @@ -45,7 +45,7 @@ fun BlurHashAsyncImage( loading = { BlurHashImage( blurHash = blurHash, - contentScale = contentScale, + contentScale = ContentScale.FillBounds, contentDescription = "Loading placeholder" ) }, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index e6ee61ee52..cba5781e11 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -26,7 +26,7 @@ import io.element.android.features.messages.impl.MessagesEvents import io.element.android.features.messages.impl.MessagesPresenter import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction -import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter +import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.libraries.designsystem.utils.SnackbarDispatcher diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 0c0b46cbe2..0b1340fd61 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -23,10 +23,10 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.messages.impl.textcomposer.AttachmentSourcePicker -import io.element.android.features.messages.impl.textcomposer.MessageComposerEvents -import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter -import io.element.android.features.messages.impl.textcomposer.MessageComposerState +import io.element.android.features.messages.impl.messagecomposer.AttachmentSourcePicker +import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents +import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter +import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.utils.SnackbarDispatcher diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index ae35449a8e..8bd6080ab3 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -209,28 +209,27 @@ class RustMatrixRoom( } } - override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result { - return runCatching { + override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result = withContext(coroutineDispatchers.io) { + runCatching { innerRoom.sendImage(file.path, thumbnailFile.path, imageInfo.map()) } } - override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result { - return runCatching { + override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result = withContext(coroutineDispatchers.io) { + runCatching { innerRoom.sendVideo(file.path, thumbnailFile.path, videoInfo.map()) } } - override suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result { - return runCatching { + override suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result = withContext(coroutineDispatchers.io) { + runCatching { innerRoom.sendAudio(file.path, audioInfo.map()) } } - override suspend fun sendFile(file: File, fileInfo: FileInfo): Result { - return runCatching { + override suspend fun sendFile(file: File, fileInfo: FileInfo): Result = withContext(coroutineDispatchers.io) { + runCatching { innerRoom.sendFile(file.path, fileInfo.map()) } } - } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt new file mode 100644 index 0000000000..416fe1d063 --- /dev/null +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -0,0 +1,55 @@ +/* + * 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.mediaupload.api + +import android.net.Uri +import io.element.android.libraries.core.extensions.flatMap +import io.element.android.libraries.matrix.api.room.MatrixRoom +import javax.inject.Inject + +class MediaSender @Inject constructor( + private val preProcessor: MediaPreProcessor, + private val room: MatrixRoom, +) { + + suspend fun sendMedia(uri: Uri, mimeType: String): Result { + return preProcessor + .process(uri, mimeType, deleteOriginal = true) + .flatMap { info -> + room.sendMedia(info) + } + } + + private suspend fun MatrixRoom.sendMedia( + info: MediaUploadInfo, + ): Result { + return when (info) { + is MediaUploadInfo.Image -> { + sendImage(info.file, info.thumbnailInfo.file, info.info) + } + + is MediaUploadInfo.Video -> { + sendVideo(info.file, info.thumbnailInfo.file, info.info) + } + + is MediaUploadInfo.AnyFile -> { + sendFile(info.file, info.info) + } + else -> error("Unexpected MediaUploadInfo format: $info") + } + } +} diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/RoomExt.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/RoomExt.kt deleted file mode 100644 index ca5ec40ae8..0000000000 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/RoomExt.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.mediaupload.api - -import io.element.android.libraries.matrix.api.room.MatrixRoom - -suspend fun MatrixRoom.sendMedia( - info: MediaUploadInfo, -): Result { - return when (info) { - is MediaUploadInfo.Image -> { - sendImage(info.file, info.thumbnailInfo.file, info.info) - } - - is MediaUploadInfo.Video -> { - sendVideo(info.file, info.thumbnailInfo.file, info.info) - } - - is MediaUploadInfo.AnyFile -> { - sendFile(info.file, info.info) - } - else -> error("Unexpected MediaUploadInfo format: $info") - } -} - diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt index 3a500abbe6..5e46f719b7 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt @@ -119,7 +119,9 @@ class AndroidMediaPreProcessor @Inject constructor( MediaUploadInfo.AnyFile(file, info) } if (deleteOriginal) { - contentResolver.delete(uri, null, null) + tryOrNull { + contentResolver.delete(uri, null, null) + } } result }.mapFailure { MediaPreProcessor.Failure(it) } From 731e0fae33bdcb58eaab7b730c223428fb4816c9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 17 May 2023 18:32:08 +0200 Subject: [PATCH 20/48] Media: show file in timeline --- .../event/TimelineItemContentView.kt | 5 ++ .../components/event/TimelineItemFileView.kt | 86 +++++++++++++++++++ .../TimelineItemContentMessageFactory.kt | 9 ++ .../model/event/TimelineItemFileContent.kt | 29 +++++++ .../event/TimelineItemFileContentProvider.kt | 36 ++++++++ 5 files changed, 165 insertions(+) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentProvider.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt index 6ef3965c5c..25f3077971 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent @@ -69,5 +70,9 @@ fun TimelineItemEventContentView( content = content, modifier = modifier ) + is TimelineItemFileContent -> TimelineItemFileView( + content = content, + modifier = modifier.defaultContentPadding() + ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt new file mode 100644 index 0000000000..0628b2050e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components.event + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Attachment +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContentProvider +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun TimelineItemFileView( + content: TimelineItemFileContent, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Filled.Attachment, + contentDescription = "OpenFile" + ) + } + Text( + text = content.body, + modifier = Modifier.padding(horizontal = 8.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Preview +@Composable +internal fun TimelineItemFileViewLightPreview(@PreviewParameter(TimelineItemFileContentProvider::class) content: TimelineItemFileContent) = + ElementPreviewLight { ContentToPreview(content) } + +@Preview +@Composable +internal fun TimelineItemFileViewDarkPreview(@PreviewParameter(TimelineItemFileContentProvider::class) content: TimelineItemFileContent) = + ElementPreviewDark { ContentToPreview(content) } + +@Composable +private fun ContentToPreview(content: TimelineItemFileContent) { + TimelineItemFileView(content) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index 4c5fa78a06..83163741c5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.factories.event import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent @@ -25,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.util.toHtmlDocument import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType @@ -66,6 +68,13 @@ class TimelineItemContentMessageFactory @Inject constructor() { aspectRatio = aspectRatio ) } + is FileMessageType -> TimelineItemFileContent( + body = messageType.body, + thumbnailSource = messageType.info?.thumbnailSource, + fileSource = messageType.source, + mimeType = messageType.info?.mimetype, + size = messageType.info?.size, + ) is NoticeMessageType -> TimelineItemNoticeContent( body = messageType.body, htmlDocument = messageType.formatted?.toHtmlDocument() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt new file mode 100644 index 0000000000..d1fb7f0942 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.model.event + +import io.element.android.libraries.matrix.api.media.MediaSource + +data class TimelineItemFileContent( + val body: String, + val fileSource: MediaSource, + val thumbnailSource: MediaSource?, + val size: Long?, + val mimeType: String?, +) : TimelineItemEventContent { + override val type: String = "TimelineItemFileContent" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentProvider.kt new file mode 100644 index 0000000000..b1217a1174 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentProvider.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.model.event + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.matrix.api.media.MediaSource + +open class TimelineItemFileContentProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aTimelineItemFileContent(), + ) +} + +fun aTimelineItemFileContent() = TimelineItemFileContent( + body = "A nice file with a nice name.pdf", + thumbnailSource = MediaSource(url = ""), + fileSource = MediaSource(url = ""), + mimeType = MimeTypes.Apk, + size = 100 +) From 2682d1c2b4af513556761d5a45c11d8d1f2ea064 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 22 May 2023 16:25:50 +0200 Subject: [PATCH 21/48] Media: send file without preview --- .../features/messages/impl/MessagesView.kt | 24 +++++-- .../MessageComposerPresenter.kt | 63 +++++++++++++++++-- .../messagecomposer/MessageComposerState.kt | 1 + 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 9e5c974949..d88a9199fd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -72,6 +72,7 @@ import io.element.android.features.messages.impl.timeline.TimelineView import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView import io.element.android.libraries.androidutils.ui.hideKeyboard +import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -87,6 +88,8 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch import timber.log.Timber +import io.element.android.libraries.ui.strings.R as StringsR + @Composable fun MessagesView( state: MessagesState, @@ -109,12 +112,7 @@ fun MessagesView( val bottomSheetState = rememberModalBottomSheetState(initialValue = initialBottomSheetState) val coroutineScope = rememberCoroutineScope() - val attachmentsState = state.composerState.attachmentsState - if (attachmentsState is AttachmentsState.Previewing) { - LaunchedEffect(attachmentsState) { - onPreviewAttachments(attachmentsState.attachments) - } - } + AttachmentStateView(state.composerState.attachmentsState, onPreviewAttachments) BackHandler(enabled = bottomSheetState.isVisible) { coroutineScope.launch { @@ -221,6 +219,20 @@ fun MessagesView( } } +@Composable +private fun AttachmentStateView( + state: AttachmentsState, + onPreviewAttachments: (ImmutableList) -> Unit +) { + when (state) { + AttachmentsState.None -> Unit + is AttachmentsState.Previewing -> LaunchedEffect(state) { + onPreviewAttachments(state.attachments) + } + is AttachmentsState.Sending -> ProgressDialog(text = stringResource(id = StringsR.string.common_loading)) + } +} + @Composable fun MessagesViewContent( state: MessagesState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 527d875f25..c2bf12228c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -27,23 +27,28 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.media3.common.MimeTypes import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.messages.impl.media.local.LocalMediaFactory import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.core.data.toStableCharSequence -import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.mediapickers.api.PickerProvider +import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.textcomposer.MessageComposerMode import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject +import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes @SingleIn(RoomScope::class) class MessageComposerPresenter @Inject constructor( @@ -52,6 +57,8 @@ class MessageComposerPresenter @Inject constructor( private val mediaPickerProvider: PickerProvider, private val featureFlagService: FeatureFlagService, private val localMediaFactory: LocalMediaFactory, + private val mediaSender: MediaSender, + private val snackbarDispatcher: SnackbarDispatcher, ) : Presenter { @SuppressLint("UnsafeOptInUsageError") @@ -69,16 +76,26 @@ class MessageComposerPresenter @Inject constructor( AttachmentsState.None } else { val mediaAttachment = Attachment.Media(localMedia) - AttachmentsState.Previewing(persistentListOf(mediaAttachment)) + val isPreviewable = when { + MimeTypes.isImage(mimeType) -> true + MimeTypes.isVideo(mimeType) -> true + MimeTypes.isAudio(mimeType) -> true + else -> false + } + if (isPreviewable) { + AttachmentsState.Previewing(persistentListOf(mediaAttachment)) + } else { + AttachmentsState.Sending(persistentListOf(mediaAttachment)) + } } } val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker(onResult = { uri, mimeType -> handlePickedMedia(uri, mimeType) }) - val filesPicker = mediaPickerProvider.registerFilePicker(MimeTypes.Any, onResult = { handlePickedMedia(it) }) - val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { handlePickedMedia(it, MimeTypes.Jpeg) }) - val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker(onResult = { handlePickedMedia(it, MimeTypes.Mp4) }) + val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes, onResult = { handlePickedMedia(it) }) + val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { handlePickedMedia(it, MimeTypes.IMAGE_JPEG) }) + val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker(onResult = { handlePickedMedia(it, MimeTypes.VIDEO_MP4) }) val isFullScreen = rememberSaveable { mutableStateOf(false) @@ -99,6 +116,13 @@ class MessageComposerPresenter @Inject constructor( } } + LaunchedEffect(attachmentsState.value) { + when (val attachmentStateValue = attachmentsState.value) { + is AttachmentsState.Sending -> localCoroutineScope.sendAttachment(attachmentStateValue.attachments.first(), attachmentsState) + else -> Unit + } + } + fun handleEvents(event: MessageComposerEvents) { when (event) { MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value @@ -177,4 +201,33 @@ class MessageComposerPresenter @Inject constructor( } } + private fun CoroutineScope.sendAttachment( + attachment: Attachment, + attachmentState: MutableState, + ) = launch { + when (attachment) { + is Attachment.Media -> { + sendMedia( + uri = attachment.localMedia.uri, + mimeType = attachment.localMedia.mimeType, + attachmentState = attachmentState + ) + } + } + } + + private suspend fun sendMedia( + uri: Uri, + mimeType: String, + attachmentState: MutableState, + ) { + mediaSender.sendMedia(uri, mimeType) + .onSuccess { + attachmentState.value = AttachmentsState.None + }.onFailure { + val snackbarMessage = SnackbarMessage(sendAttachmentError(it)) + snackbarDispatcher.post(snackbarMessage) + attachmentState.value = AttachmentsState.None + } + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index 9df3e2269a..12965b7e55 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -38,6 +38,7 @@ data class MessageComposerState( sealed interface AttachmentsState { object None : AttachmentsState data class Previewing(val attachments: ImmutableList) : AttachmentsState + data class Sending(val attachments: ImmutableList) : AttachmentsState } sealed interface AttachmentSourcePicker { From 458cd2d4f3661eaa138a6eead1fb869e7f766ad0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 22 May 2023 20:24:42 +0200 Subject: [PATCH 22/48] Media: keep the name of the file when possible --- .../messages/impl/attachments/Attachment.kt | 2 +- .../preview/AttachmentsPreviewPresenter.kt | 9 ++--- .../AttachmentsPreviewStateProvider.kt | 1 + .../MessageComposerPresenter.kt | 14 ++++---- .../libraries/androidutils/file/Context.kt | 35 +++++++++++++++++++ .../libraries/androidutils/file/File.kt | 6 ++-- .../mediaupload/api/MediaPreProcessor.kt | 3 +- .../libraries/mediaupload/api/MediaSender.kt | 9 +++-- .../mediaupload/AndroidMediaPreProcessor.kt | 35 ++++++++++++++----- .../mediaupload/test/FakeMediaPreProcessor.kt | 7 +++- 10 files changed, 90 insertions(+), 31 deletions(-) create mode 100644 libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt index 53626a5037..8739a45201 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt @@ -25,5 +25,5 @@ import kotlinx.parcelize.Parcelize sealed interface Attachment : Parcelable { @Parcelize - data class Media(val localMedia: LocalMedia) : Attachment + data class Media(val localMedia: LocalMedia, val compressIfPossible: Boolean) : Attachment } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt index edc0248d18..d80359e88c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -16,7 +16,6 @@ package io.element.android.features.messages.impl.attachments.preview -import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf @@ -73,8 +72,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( when (attachment) { is Attachment.Media -> { sendMedia( - uri = attachment.localMedia.uri, - mimeType = attachment.localMedia.mimeType, + mediaAttachment = attachment, sendActionState = sendActionState ) } @@ -82,12 +80,11 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( } private suspend fun sendMedia( - uri: Uri, - mimeType: String, + mediaAttachment: Attachment.Media, sendActionState: MutableState>, ) { suspend { - mediaSender.sendMedia(uri, mimeType) + mediaSender.sendMedia(mediaAttachment.localMedia.uri, mediaAttachment.localMedia.mimeType, mediaAttachment.compressIfPossible) }.executeResult(sendActionState) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt index 151698b0f4..220b4554dc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt @@ -36,6 +36,7 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider = Async.Uninitialized) = AttachmentsPreviewState( attachment = Attachment.Media( localMedia = LocalMedia("".toUri(), mimeType = MimeTypes.OctetStream), + compressIfPossible = true ), sendActionState = sendActionState, eventSink = {} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index c2bf12228c..1b1bebf209 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -70,16 +70,16 @@ class MessageComposerPresenter @Inject constructor( mutableStateOf(AttachmentsState.None) } - fun handlePickedMedia(uri: Uri?, mimeType: String? = null) { + fun handlePickedMedia(uri: Uri?, mimeType: String? = null, compressIfPossible: Boolean = true) { val localMedia = localMediaFactory.createFromUri(uri, mimeType) attachmentsState.value = if (localMedia == null) { AttachmentsState.None } else { - val mediaAttachment = Attachment.Media(localMedia) + val mediaAttachment = Attachment.Media(localMedia, compressIfPossible) val isPreviewable = when { - MimeTypes.isImage(mimeType) -> true - MimeTypes.isVideo(mimeType) -> true - MimeTypes.isAudio(mimeType) -> true + MimeTypes.isImage(localMedia.mimeType) -> true + MimeTypes.isVideo(localMedia.mimeType) -> true + MimeTypes.isAudio(localMedia.mimeType) -> true else -> false } if (isPreviewable) { @@ -93,7 +93,7 @@ class MessageComposerPresenter @Inject constructor( val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker(onResult = { uri, mimeType -> handlePickedMedia(uri, mimeType) }) - val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes, onResult = { handlePickedMedia(it) }) + val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes, onResult = { handlePickedMedia(it, compressIfPossible = false) }) val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { handlePickedMedia(it, MimeTypes.IMAGE_JPEG) }) val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker(onResult = { handlePickedMedia(it, MimeTypes.VIDEO_MP4) }) @@ -221,7 +221,7 @@ class MessageComposerPresenter @Inject constructor( mimeType: String, attachmentState: MutableState, ) { - mediaSender.sendMedia(uri, mimeType) + mediaSender.sendMedia(uri, mimeType, compressIfPossible = false) .onSuccess { attachmentState.value = AttachmentsState.None }.onFailure { diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt new file mode 100644 index 0000000000..a5fab7545c --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt @@ -0,0 +1,35 @@ +/* + * 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.androidutils.file + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns +import java.io.File + +fun Context.getFileName(uri: Uri): String? = when (uri.scheme) { + ContentResolver.SCHEME_CONTENT -> getContentFileName(uri) + else -> uri.path?.let(::File)?.name +} + +private fun Context.getContentFileName(uri: Uri): String? = runCatching { + contentResolver.query(uri, null, null, null, null)?.use { cursor -> + cursor.moveToFirst() + return@use cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME).let(cursor::getString) + } +}.getOrNull() diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt index 80df69cbcc..581d45a2b0 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt @@ -18,8 +18,6 @@ package io.element.android.libraries.androidutils.file import android.content.Context import io.element.android.libraries.core.data.tryOrNull -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import timber.log.Timber import java.io.File import java.util.UUID @@ -37,7 +35,7 @@ fun File.safeDelete() { ) } -suspend fun Context.createTmpFile(baseDir: File = cacheDir, extension: String? = null): File = withContext(Dispatchers.IO) { +fun Context.createTmpFile(baseDir: File = cacheDir, extension: String? = null): File { val suffix = extension?.let { ".$extension" } - File.createTempFile(UUID.randomUUID().toString(), suffix, baseDir).apply { mkdirs() } + return File.createTempFile(UUID.randomUUID().toString(), suffix, baseDir).apply { mkdirs() } } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt index 2144f7bdd7..31c6a813ff 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaPreProcessor.kt @@ -27,7 +27,8 @@ interface MediaPreProcessor { suspend fun process( uri: Uri, mimeType: String, - deleteOriginal: Boolean = false + deleteOriginal: Boolean = false, + compressIfPossible: Boolean ): Result data class Failure(override val cause: Throwable?) : RuntimeException(cause) diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index 416fe1d063..d08860d4ca 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -26,9 +26,14 @@ class MediaSender @Inject constructor( private val room: MatrixRoom, ) { - suspend fun sendMedia(uri: Uri, mimeType: String): Result { + suspend fun sendMedia(uri: Uri, mimeType: String, compressIfPossible: Boolean): Result { return preProcessor - .process(uri, mimeType, deleteOriginal = true) + .process( + uri = uri, + mimeType = mimeType, + deleteOriginal = true, + compressIfPossible = compressIfPossible + ) .flatMap { info -> room.sendMedia(info) } diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt index 5e46f719b7..a4b5b5ea63 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt @@ -23,7 +23,9 @@ import android.net.Uri import androidx.exifinterface.media.ExifInterface import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.androidutils.file.createTmpFile +import io.element.android.libraries.androidutils.file.getFileName import io.element.android.libraries.androidutils.media.runAndRelease +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.mimetype.MimeTypes @@ -41,7 +43,6 @@ import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onEach @@ -58,6 +59,7 @@ class AndroidMediaPreProcessor @Inject constructor( @ApplicationContext private val context: Context, private val imageCompressor: ImageCompressor, private val videoCompressor: VideoCompressor, + private val coroutineDispatchers: CoroutineDispatchers, ) : MediaPreProcessor { companion object { /** @@ -92,12 +94,13 @@ class AndroidMediaPreProcessor @Inject constructor( uri: Uri, mimeType: String, deleteOriginal: Boolean, + compressIfPossible: Boolean, ): Result = runCatching { - val compressBeforeSending = ( - mimeType.isMimeTypeImage() && mimeType != MimeTypes.Gif) || + val shouldBeCompressed = compressIfPossible && + (mimeType.isMimeTypeImage() && mimeType != MimeTypes.Gif) || mimeType.isMimeTypeVideo() - val result = if (compressBeforeSending) { + val result = if (shouldBeCompressed) { when { mimeType.isMimeTypeImage() -> processImage(uri) mimeType.isMimeTypeVideo() -> processVideo(uri, mimeType) @@ -123,9 +126,26 @@ class AndroidMediaPreProcessor @Inject constructor( contentResolver.delete(uri, null, null) } } - result + result.postProcess(uri) }.mapFailure { MediaPreProcessor.Failure(it) } + private fun MediaUploadInfo.postProcess(uri: Uri): MediaUploadInfo { + + fun File.rename(name: String): File { + return File(context.cacheDir, name).also { + renameTo(it) + } + } + + val name = context.getFileName(uri) ?: return this + return when (this) { + is MediaUploadInfo.AnyFile -> copy(file = file.rename(name)) + is MediaUploadInfo.Audio -> copy(file = file.rename(name)) + is MediaUploadInfo.Image -> copy(file = file.rename(name)) + is MediaUploadInfo.Video -> copy(file = file.rename(name)) + } + } + private suspend fun processImage(uri: Uri): MediaUploadInfo { val compressedFileResult = contentResolver.openInputStream(uri).use { input -> imageCompressor.compressToTmpFile( @@ -176,7 +196,6 @@ class AndroidMediaPreProcessor @Inject constructor( inputStream = inputStream, resizeMode = ResizeMode.Strict(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT), ).getOrThrow() - return thumbnailResult.toThumbnailProcessingInfo(MimeTypes.Jpeg) } @@ -191,7 +210,7 @@ class AndroidMediaPreProcessor @Inject constructor( } private suspend fun createTmpFileWithInput(inputStream: InputStream): File? { - return withContext(Dispatchers.IO) { + return withContext(coroutineDispatchers.io) { tryOrNull { val tmpFile = context.createTmpFile() tmpFile.outputStream().use { inputStream.copyTo(it) } @@ -203,7 +222,6 @@ class AndroidMediaPreProcessor @Inject constructor( private fun extractVideoMetadata(file: File, mimeType: String?, thumbnailUrl: String?, thumbnailInfo: ThumbnailProcessingInfo?): VideoInfo = MediaMetadataRetriever().runAndRelease { setDataSource(context, Uri.fromFile(file)) - VideoInfo( duration = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L, width = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toLong() ?: 0L, @@ -229,7 +247,6 @@ class AndroidMediaPreProcessor @Inject constructor( inputStream = inputStream, resizeMode = ResizeMode.Strict(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT), ) - result.getOrThrow().toThumbnailProcessingInfo(MimeTypes.Jpeg) } diff --git a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt index f8b7237bbe..c4ab8d57b1 100644 --- a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt +++ b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt @@ -36,7 +36,12 @@ class FakeMediaPreProcessor : MediaPreProcessor { ) ) - override suspend fun process(uri: Uri, mimeType: String, deleteOriginal: Boolean): Result = result + override suspend fun process( + uri: Uri, + mimeType: String, + deleteOriginal: Boolean, + compressIfPossible: Boolean + ): Result = result fun givenResult(value: Result) { this.result = value From 319f426b064868ba338bbc572dc63707ed1a16b0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 22 May 2023 21:24:43 +0200 Subject: [PATCH 23/48] Media: align attachement source picker design with Figma --- .../messages/impl/MessagesStateProvider.kt | 4 +- .../features/messages/impl/MessagesView.kt | 74 ++++++++----------- .../messagecomposer/MessageComposerEvents.kt | 7 +- .../MessageComposerPresenter.kt | 23 +++--- .../messagecomposer/MessageComposerState.kt | 7 +- .../MessageComposerStateProvider.kt | 2 +- .../MessageComposerPresenterTest.kt | 7 +- 7 files changed, 48 insertions(+), 76 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index d235232560..1919de78f0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -18,7 +18,6 @@ package io.element.android.features.messages.impl import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.messages.impl.actionlist.anActionListState -import io.element.android.features.messages.impl.messagecomposer.AttachmentSourcePicker import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.timeline.aTimelineItemContent import io.element.android.features.messages.impl.timeline.aTimelineItemList @@ -33,8 +32,7 @@ open class MessagesStateProvider : PreviewParameterProvider { get() = sequenceOf( aMessagesState(), aMessagesState().copy(hasNetworkConnection = false), - aMessagesState().copy(composerState = aMessageComposerState().copy(attachmentSourcePicker = AttachmentSourcePicker.AllMedia)), - aMessagesState().copy(composerState = aMessageComposerState().copy(attachmentSourcePicker = AttachmentSourcePicker.Camera)), + aMessagesState().copy(composerState = aMessageComposerState().copy(showAttachmentSourcePicker = true)), ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index a3b64db002..be38b7a59c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -37,6 +37,11 @@ import androidx.compose.material.ListItem import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.AttachFile +import androidx.compose.material.icons.filled.Collections +import androidx.compose.material.icons.filled.LocalLibrary +import androidx.compose.material.icons.filled.PhotoCamera +import androidx.compose.material.icons.filled.Videocam import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SnackbarHost @@ -61,7 +66,6 @@ import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.attachments.Attachment -import io.element.android.features.messages.impl.messagecomposer.AttachmentSourcePicker import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerView @@ -102,7 +106,7 @@ fun MessagesView( initialValue = ModalBottomSheetValue.Hidden, ) val composerState = state.composerState - val initialBottomSheetState = if (LocalInspectionMode.current && composerState.attachmentSourcePicker != null) { + val initialBottomSheetState = if (LocalInspectionMode.current && composerState.showAttachmentSourcePicker != null) { ModalBottomSheetValue.Expanded } else { ModalBottomSheetValue.Hidden @@ -154,8 +158,8 @@ fun MessagesView( state.eventSink(MessagesEvents.HandleAction(action, event)) } - LaunchedEffect(composerState.attachmentSourcePicker) { - if (composerState.attachmentSourcePicker != null) { + LaunchedEffect(composerState.showAttachmentSourcePicker) { + if (composerState.showAttachmentSourcePicker) { // We need to use this instead of `LocalFocusManager.clearFocus()` to hide the keyboard when focus is on an Android View localView.hideKeyboard() bottomSheetState.show() @@ -173,8 +177,7 @@ fun MessagesView( sheetState = bottomSheetState, displayHandle = true, sheetContent = { - MediaPickerMenu( - addAttachmentSourcePicker = composerState.attachmentSourcePicker, + AttachmentSourcePickerMenu( eventSink = composerState.eventSink ) } @@ -305,50 +308,33 @@ fun MessagesViewTopBar( ) } -@Composable -internal fun MediaPickerMenu( - addAttachmentSourcePicker: AttachmentSourcePicker?, - eventSink: (MessageComposerEvents) -> Unit, -) { - when (addAttachmentSourcePicker) { - null -> return - AttachmentSourcePicker.AllMedia -> AllMediaSourcePickerMenu(eventSink = eventSink) - AttachmentSourcePicker.Camera -> CameraSourcePickerMenu(eventSink = eventSink) - } -} - @OptIn(ExperimentalMaterialApi::class) @Composable -internal fun AllMediaSourcePickerMenu( +internal fun AttachmentSourcePickerMenu( eventSink: (MessageComposerEvents) -> Unit, modifier: Modifier = Modifier, ) { Column(modifier) { - ListItem(Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) }) { - Text(stringResource(R.string.screen_room_attachment_source_gallery)) - } - ListItem(Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) }) { - Text(stringResource(R.string.screen_room_attachment_source_files)) - } - ListItem(Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromCamera) }) { - Text(stringResource(R.string.screen_room_attachment_source_camera)) - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -internal fun CameraSourcePickerMenu( - eventSink: (MessageComposerEvents) -> Unit, - modifier: Modifier = Modifier, -) { - Column(modifier) { - ListItem(Modifier.clickable { eventSink(MessageComposerEvents.PickCameraAttachmentSource.Photo) }) { - Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) - } - ListItem(Modifier.clickable { eventSink(MessageComposerEvents.PickCameraAttachmentSource.Video) }) { - Text(stringResource(R.string.screen_room_attachment_source_camera_video)) - } + ListItem( + modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) }, + icon = { Icon(Icons.Default.Collections, null) }, + text = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) }, + ) + ListItem( + modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) }, + icon = { Icon(Icons.Default.AttachFile, null) }, + text = { Text(stringResource(R.string.screen_room_attachment_source_files)) }, + ) + ListItem( + modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) }, + icon = { Icon(Icons.Default.PhotoCamera, null) }, + text = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) }, + ) + ListItem( + modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) }, + icon = { Icon(Icons.Default.Videocam, null) }, + text = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) }, + ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt index 92ce191ce1..155e981fcc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt @@ -30,11 +30,8 @@ sealed interface MessageComposerEvents { object DismissAttachmentMenu : MessageComposerEvents sealed interface PickAttachmentSource : MessageComposerEvents { object FromGallery : PickAttachmentSource - object FromCamera : PickAttachmentSource object FromFiles : PickAttachmentSource - } - sealed interface PickCameraAttachmentSource : MessageComposerEvents { - object Photo : PickCameraAttachmentSource - object Video : PickCameraAttachmentSource + object PhotoFromCamera : PickAttachmentSource + object VideoFromCamera : PickAttachmentSource } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 1b1bebf209..0a93834231 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -107,7 +107,7 @@ class MessageComposerPresenter @Inject constructor( mutableStateOf(MessageComposerMode.Normal("")) } - var attachmentSourcePicker: AttachmentSourcePicker? by remember { mutableStateOf(null) } + var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) } LaunchedEffect(composerMode.value) { when (val modeValue = composerMode.value) { @@ -135,26 +135,23 @@ class MessageComposerPresenter @Inject constructor( is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text) is MessageComposerEvents.SetMode -> composerMode.value = event.composerMode MessageComposerEvents.AddAttachment -> localCoroutineScope.ifMediaPickersEnabled { - attachmentSourcePicker = AttachmentSourcePicker.AllMedia + showAttachmentSourcePicker = true } - MessageComposerEvents.DismissAttachmentMenu -> attachmentSourcePicker = null + MessageComposerEvents.DismissAttachmentMenu -> showAttachmentSourcePicker = false MessageComposerEvents.PickAttachmentSource.FromGallery -> localCoroutineScope.ifMediaPickersEnabled { - attachmentSourcePicker = null + showAttachmentSourcePicker = false galleryMediaPicker.launch() } MessageComposerEvents.PickAttachmentSource.FromFiles -> localCoroutineScope.ifMediaPickersEnabled { - attachmentSourcePicker = null + showAttachmentSourcePicker = false filesPicker.launch() } - MessageComposerEvents.PickAttachmentSource.FromCamera -> localCoroutineScope.ifMediaPickersEnabled { - attachmentSourcePicker = AttachmentSourcePicker.Camera - } - MessageComposerEvents.PickCameraAttachmentSource.Photo -> localCoroutineScope.ifMediaPickersEnabled { - attachmentSourcePicker = null + MessageComposerEvents.PickAttachmentSource.PhotoFromCamera -> localCoroutineScope.ifMediaPickersEnabled { + showAttachmentSourcePicker = false cameraPhotoPicker.launch() } - MessageComposerEvents.PickCameraAttachmentSource.Video -> localCoroutineScope.ifMediaPickersEnabled { - attachmentSourcePicker = null + MessageComposerEvents.PickAttachmentSource.VideoFromCamera -> localCoroutineScope.ifMediaPickersEnabled { + showAttachmentSourcePicker = false cameraVideoPicker.launch() } } @@ -164,7 +161,7 @@ class MessageComposerPresenter @Inject constructor( text = text.value, isFullScreen = isFullScreen.value, mode = composerMode.value, - attachmentSourcePicker = attachmentSourcePicker, + showAttachmentSourcePicker = showAttachmentSourcePicker, attachmentsState = attachmentsState.value, eventSink = ::handleEvents ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index 12965b7e55..9c6a2c650a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -27,7 +27,7 @@ data class MessageComposerState( val text: StableCharSequence?, val isFullScreen: Boolean, val mode: MessageComposerMode, - val attachmentSourcePicker: AttachmentSourcePicker?, + val showAttachmentSourcePicker: Boolean, val attachmentsState: AttachmentsState, val eventSink: (MessageComposerEvents) -> Unit ) { @@ -40,8 +40,3 @@ sealed interface AttachmentsState { data class Previewing(val attachments: ImmutableList) : AttachmentsState data class Sending(val attachments: ImmutableList) : AttachmentsState } - -sealed interface AttachmentSourcePicker { - object AllMedia : AttachmentSourcePicker - object Camera : AttachmentSourcePicker -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index b55a168814..56d050e7b1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -31,7 +31,7 @@ fun aMessageComposerState() = MessageComposerState( text = StableCharSequence(""), isFullScreen = false, mode = MessageComposerMode.Normal(content = ""), - attachmentSourcePicker = null, + showAttachmentSourcePicker = false, attachmentsState = AttachmentsState.None, eventSink = {} ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 1f6f172c25..65740b6485 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -44,7 +44,6 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor -import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor @@ -259,7 +258,7 @@ class MessageComposerPresenterTest { val initialState = awaitItem() initialState.eventSink(MessageComposerEvents.AddAttachment) - assertThat(awaitItem().attachmentSourcePicker).isEqualTo(AttachmentSourcePicker.AllMedia) + assertThat(awaitItem().showAttachmentSourcePicker).isEqualTo(AttachmentSourcePicker.AllMedia) } } @@ -272,7 +271,7 @@ class MessageComposerPresenterTest { val initialState = awaitItem() initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromCamera) - assertThat(awaitItem().attachmentSourcePicker).isEqualTo(AttachmentSourcePicker.Camera) + assertThat(awaitItem().showAttachmentSourcePicker).isEqualTo(AttachmentSourcePicker.Camera) } } @@ -287,7 +286,7 @@ class MessageComposerPresenterTest { skipItems(1) initialState.eventSink(MessageComposerEvents.DismissAttachmentMenu) - assertThat(awaitItem().attachmentSourcePicker).isNull() + assertThat(awaitItem().showAttachmentSourcePicker).isNull() } } From 5c198bc279e6dd49ed7c33a56ee7f81e31761180 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 23 May 2023 16:58:22 +0200 Subject: [PATCH 24/48] Media: fix zoomable image with non content uri --- .../messages/impl/media/local/LocalMedia.kt | 9 +++- .../impl/media/local/LocalMediaView.kt | 22 +++++--- .../impl/media/local/UriToFileMapper.kt | 50 +++++++++++++++++++ .../androidutils/uri/UriExtensions.kt | 4 ++ 4 files changed, 76 insertions(+), 9 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/UriToFileMapper.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt index c26a4baaff..5270621c2d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt @@ -18,10 +18,17 @@ package io.element.android.features.messages.impl.media.local import android.net.Uri import android.os.Parcelable +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize data class LocalMedia( val uri: Uri, val mimeType: String, -) : Parcelable +) : Parcelable { + + /** + * This tries to convert the uri to a file if applicable, otherwise keep it as uri. + */ + @IgnoredOnParcel val model: Any = UriToFileMapper.map(uri) ?: uri +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt index b5591c14f2..f814dd6662 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt @@ -17,7 +17,6 @@ package io.element.android.features.messages.impl.media.local import android.annotation.SuppressLint -import android.net.Uri import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout import androidx.compose.foundation.layout.fillMaxSize @@ -36,7 +35,10 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView import io.element.android.libraries.designsystem.utils.OnLifecycleEvent +import me.saket.telephoto.zoomable.ZoomSpec import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage +import me.saket.telephoto.zoomable.rememberZoomableImageState +import me.saket.telephoto.zoomable.rememberZoomableState @SuppressLint("UnsafeOptInUsageError") @Composable @@ -46,11 +48,11 @@ fun LocalMediaView( ) { when { MimeTypes.isImage(localMedia.mimeType) -> MediaImageView( - uri = localMedia.uri, + localMedia = localMedia, modifier = modifier ) MimeTypes.isVideo(localMedia.mimeType) -> MediaVideoView( - uri = localMedia.uri, + localMedia = localMedia, modifier = modifier ) else -> Unit @@ -59,12 +61,16 @@ fun LocalMediaView( @Composable private fun MediaImageView( - uri: Uri, + localMedia: LocalMedia, modifier: Modifier = Modifier, ) { + val zoomableState = rememberZoomableState( + zoomSpec = ZoomSpec(maxZoomFactor = 3f) + ) ZoomableAsyncImage( modifier = modifier.fillMaxSize(), - model = uri, + state = rememberZoomableImageState(zoomableState), + model = localMedia.model, contentDescription = "Image", contentScale = ContentScale.Fit, ) @@ -73,7 +79,7 @@ private fun MediaImageView( @UnstableApi @Composable fun MediaVideoView( - uri: Uri, + localMedia: LocalMedia, modifier: Modifier = Modifier, ) { val context = LocalContext.current @@ -84,8 +90,8 @@ fun MediaVideoView( this.prepare() } } - LaunchedEffect(uri) { - val mediaItem = MediaItem.fromUri(uri) + LaunchedEffect(localMedia.uri) { + val mediaItem = MediaItem.fromUri(localMedia.uri) exoPlayer.setMediaItem(mediaItem) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/UriToFileMapper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/UriToFileMapper.kt new file mode 100644 index 0000000000..417219c60e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/UriToFileMapper.kt @@ -0,0 +1,50 @@ +/* + * 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.local + +import android.content.ContentResolver +import android.net.Uri +import io.element.android.libraries.androidutils.uri.ASSET_FILE_PATH_ROOT +import io.element.android.libraries.androidutils.uri.firstPathSegment +import java.io.File + +/** + * Tries to convert a URI to a File. + * Extracted from Coil [coil.map.FileUriMapper] + */ +object UriToFileMapper { + + fun map(data: Uri): File? { + if (!isApplicable(data)) return null + return if (data.scheme == ContentResolver.SCHEME_FILE) { + data.path?.let(::File) + } else { + // If the scheme is not "file", it's null, representing a literal path on disk. + // Assume the entire input, regardless of any reserved characters, is valid. + File(data.toString()) + } + } + + private fun isApplicable(data: Uri): Boolean { + return data.scheme.let { it == null || it == ContentResolver.SCHEME_FILE } && + data.path.orEmpty().startsWith('/') && data.firstPathSegment != null + } + + private fun isAssetUri(uri: Uri): Boolean { + return uri.scheme == ContentResolver.SCHEME_FILE && uri.firstPathSegment == ASSET_FILE_PATH_ROOT + } +} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/uri/UriExtensions.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/uri/UriExtensions.kt index 485a103b5b..1375104b79 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/uri/UriExtensions.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/uri/UriExtensions.kt @@ -18,8 +18,12 @@ package io.element.android.libraries.androidutils.uri import android.net.Uri +const val ASSET_FILE_PATH_ROOT = "android_asset" const val IGNORED_SCHEMA = "ignored" fun Uri.isIgnored() = scheme == IGNORED_SCHEMA fun createIgnoredUri(path: String): Uri = Uri.parse("$IGNORED_SCHEMA://$path") + +val Uri.firstPathSegment: String? + get() = pathSegments.firstOrNull() From 0a268dc27f8c78bbe5763e98ef67282c8e84de75 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 23 May 2023 21:40:18 +0200 Subject: [PATCH 25/48] Media: make existing tests passes on MessagesPresenters --- .../messages/impl/media/local/LocalMedia.kt | 4 +- .../MessageComposerPresenter.kt | 78 ++++++++------- .../messages/MessagesPresenterTest.kt | 5 +- .../MessageComposerPresenterTest.kt | 94 ++++++++----------- 4 files changed, 89 insertions(+), 92 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt index 5270621c2d..016fa382c3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt @@ -30,5 +30,7 @@ data class LocalMedia( /** * This tries to convert the uri to a file if applicable, otherwise keep it as uri. */ - @IgnoredOnParcel val model: Any = UriToFileMapper.map(uri) ?: uri + @IgnoredOnParcel val model: Any by lazy { + UriToFileMapper.map(uri) ?: uri + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 0a93834231..a1c1e3e6d1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.messages.impl.media.local.LocalMediaFactory @@ -70,33 +71,18 @@ class MessageComposerPresenter @Inject constructor( mutableStateOf(AttachmentsState.None) } - fun handlePickedMedia(uri: Uri?, mimeType: String? = null, compressIfPossible: Boolean = true) { - val localMedia = localMediaFactory.createFromUri(uri, mimeType) - attachmentsState.value = if (localMedia == null) { - AttachmentsState.None - } else { - val mediaAttachment = Attachment.Media(localMedia, compressIfPossible) - val isPreviewable = when { - MimeTypes.isImage(localMedia.mimeType) -> true - MimeTypes.isVideo(localMedia.mimeType) -> true - MimeTypes.isAudio(localMedia.mimeType) -> true - else -> false - } - if (isPreviewable) { - AttachmentsState.Previewing(persistentListOf(mediaAttachment)) - } else { - AttachmentsState.Sending(persistentListOf(mediaAttachment)) - } - } + val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType -> + handlePickedMedia(attachmentsState, uri, mimeType) + } + val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri -> + handlePickedMedia(attachmentsState, uri, compressIfPossible = false) + } + val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri -> + handlePickedMedia(attachmentsState, uri, MimeTypes.IMAGE_JPEG) + } + val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker { uri -> + handlePickedMedia(attachmentsState, uri, MimeTypes.VIDEO_MP4) } - - val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker(onResult = { uri, mimeType -> - handlePickedMedia(uri, mimeType) - }) - val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes, onResult = { handlePickedMedia(it, compressIfPossible = false) }) - val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { handlePickedMedia(it, MimeTypes.IMAGE_JPEG) }) - val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker(onResult = { handlePickedMedia(it, MimeTypes.VIDEO_MP4) }) - val isFullScreen = rememberSaveable { mutableStateOf(false) } @@ -107,7 +93,7 @@ class MessageComposerPresenter @Inject constructor( mutableStateOf(MessageComposerMode.Normal("")) } - var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) } + var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) } LaunchedEffect(composerMode.value) { when (val modeValue = composerMode.value) { @@ -134,23 +120,23 @@ class MessageComposerPresenter @Inject constructor( is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage(event.message, composerMode, text) is MessageComposerEvents.SetMode -> composerMode.value = event.composerMode - MessageComposerEvents.AddAttachment -> localCoroutineScope.ifMediaPickersEnabled { + MessageComposerEvents.AddAttachment -> localCoroutineScope.launchIfMediaPickerEnabled { showAttachmentSourcePicker = true } MessageComposerEvents.DismissAttachmentMenu -> showAttachmentSourcePicker = false - MessageComposerEvents.PickAttachmentSource.FromGallery -> localCoroutineScope.ifMediaPickersEnabled { + MessageComposerEvents.PickAttachmentSource.FromGallery -> localCoroutineScope.launchIfMediaPickerEnabled { showAttachmentSourcePicker = false galleryMediaPicker.launch() } - MessageComposerEvents.PickAttachmentSource.FromFiles -> localCoroutineScope.ifMediaPickersEnabled { + MessageComposerEvents.PickAttachmentSource.FromFiles -> localCoroutineScope.launchIfMediaPickerEnabled { showAttachmentSourcePicker = false filesPicker.launch() } - MessageComposerEvents.PickAttachmentSource.PhotoFromCamera -> localCoroutineScope.ifMediaPickersEnabled { + MessageComposerEvents.PickAttachmentSource.PhotoFromCamera -> localCoroutineScope.launchIfMediaPickerEnabled { showAttachmentSourcePicker = false cameraPhotoPicker.launch() } - MessageComposerEvents.PickAttachmentSource.VideoFromCamera -> localCoroutineScope.ifMediaPickersEnabled { + MessageComposerEvents.PickAttachmentSource.VideoFromCamera -> localCoroutineScope.launchIfMediaPickerEnabled { showAttachmentSourcePicker = false cameraVideoPicker.launch() } @@ -167,7 +153,7 @@ class MessageComposerPresenter @Inject constructor( ) } - private fun CoroutineScope.ifMediaPickersEnabled(action: suspend () -> Unit) = launch { + private fun CoroutineScope.launchIfMediaPickerEnabled(action: suspend () -> Unit) = launch { if (featureFlagService.isFeatureEnabled(FeatureFlags.ShowMediaUploadingFlow)) { action() } @@ -213,6 +199,32 @@ class MessageComposerPresenter @Inject constructor( } } + @UnstableApi + private fun handlePickedMedia( + attachmentsState: MutableState, + uri: Uri?, + mimeType: String? = null, + compressIfPossible: Boolean = true, + ) { + val localMedia = localMediaFactory.createFromUri(uri, mimeType) + attachmentsState.value = if (localMedia == null) { + AttachmentsState.None + } else { + val mediaAttachment = Attachment.Media(localMedia, compressIfPossible) + val isPreviewable = when { + MimeTypes.isImage(localMedia.mimeType) -> true + MimeTypes.isVideo(localMedia.mimeType) -> true + MimeTypes.isAudio(localMedia.mimeType) -> true + else -> false + } + if (isPreviewable) { + AttachmentsState.Previewing(persistentListOf(mediaAttachment)) + } else { + AttachmentsState.Sending(persistentListOf(mediaAttachment)) + } + } + } + private suspend fun sendMedia( uri: Uri, mimeType: String, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index cba5781e11..93db073f78 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.MessagesEvents import io.element.android.features.messages.impl.MessagesPresenter import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.media.local.FakeLocalMediaFactory import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.networkmonitor.test.FakeNetworkMonitor @@ -36,6 +37,7 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.mediapickers.test.FakePickerProvider +import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.textcomposer.MessageComposerMode import kotlinx.coroutines.test.TestScope @@ -133,7 +135,8 @@ class MessagesPresenterTest { room = matrixRoom, mediaPickerProvider = FakePickerProvider(), featureFlagService = FakeFeatureFlagService(), - mediaPreProcessor = FakeMediaPreProcessor(), + localMediaFactory = FakeLocalMediaFactory(), + mediaSender = MediaSender(FakeMediaPreProcessor(),matrixRoom), snackbarDispatcher = SnackbarDispatcher(), ) val timelinePresenter = TimelinePresenter( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 65740b6485..1736672c98 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -21,7 +21,8 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.messages.impl.messagecomposer.AttachmentSourcePicker +import io.element.android.features.messages.impl.media.local.FakeLocalMediaFactory +import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.messagecomposer.MessageComposerState @@ -44,13 +45,13 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor +import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.textcomposer.MessageComposerMode import io.mockk.mockk import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -61,13 +62,12 @@ class MessageComposerPresenterTest { private val pickerProvider = FakePickerProvider().apply { givenResult(mockk()) // Uri is not available in JVM, so the only way to have a non-null Uri is using Mockk } - private val featureFlagService = FakeFeatureFlagService().apply { - runBlocking { - setFeatureEnabled(FeatureFlags.ShowMediaUploadingFlow, true) - } - } + private val featureFlagService = FakeFeatureFlagService( + mapOf(FeatureFlags.ShowMediaUploadingFlow.key to true) + ) private val mediaPreProcessor = FakeMediaPreProcessor() private val snackbarDispatcher = SnackbarDispatcher() + private val localMediaFactory = FakeLocalMediaFactory() @Test fun `present - initial state`() = runTest { @@ -79,6 +79,8 @@ class MessageComposerPresenterTest { assertThat(initialState.isFullScreen).isFalse() assertThat(initialState.text).isEqualTo(StableCharSequence("")) assertThat(initialState.mode).isEqualTo(MessageComposerMode.Normal("")) + assertThat(initialState.showAttachmentSourcePicker).isFalse() + assertThat(initialState.attachmentsState).isEqualTo(AttachmentsState.None) assertThat(initialState.isSendButtonVisible).isFalse() } } @@ -256,22 +258,9 @@ class MessageComposerPresenterTest { presenter.present() }.test { val initialState = awaitItem() + assertThat(initialState.showAttachmentSourcePicker).isEqualTo(false) initialState.eventSink(MessageComposerEvents.AddAttachment) - - assertThat(awaitItem().showAttachmentSourcePicker).isEqualTo(AttachmentSourcePicker.AllMedia) - } - } - - @Test - fun `present - Open camera attachments menu`() = runTest { - val presenter = createPresenter(this) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromCamera) - - assertThat(awaitItem().showAttachmentSourcePicker).isEqualTo(AttachmentSourcePicker.Camera) + assertThat(awaitItem().showAttachmentSourcePicker).isEqualTo(true) } } @@ -286,7 +275,7 @@ class MessageComposerPresenterTest { skipItems(1) initialState.eventSink(MessageComposerEvents.DismissAttachmentMenu) - assertThat(awaitItem().showAttachmentSourcePicker).isNull() + assertThat(awaitItem().showAttachmentSourcePicker).isFalse() } } @@ -326,9 +315,9 @@ class MessageComposerPresenterTest { }.test { val initialState = awaitItem() initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) - // Wait for the launched upload coroutine to run - runCurrent() - assertThat(room.sendMediaCount).isEqualTo(1) + val previewingState = awaitItem() + assertThat(previewingState.showAttachmentSourcePicker).isFalse() + assertThat(previewingState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java) } } @@ -369,22 +358,9 @@ class MessageComposerPresenterTest { }.test { val initialState = awaitItem() initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) - // Wait for the launched upload coroutine to run - runCurrent() - assertThat(room.sendMediaCount).isEqualTo(1) - } - } - - @Test - fun `present - Pick media from gallery fails if returned mimetype is not video or image`() = runTest { - val presenter = createPresenter(this) - pickerProvider.givenMimeType(MimeTypes.Audio) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) - assertThat(awaitError()).isInstanceOf(IllegalStateException::class.java) + val previewingState = awaitItem() + assertThat(previewingState.showAttachmentSourcePicker).isFalse() + assertThat(previewingState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java) } } @@ -413,9 +389,10 @@ class MessageComposerPresenterTest { }.test { val initialState = awaitItem() initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) - // Wait for the launched upload coroutine to run - runCurrent() - assertThat(room.sendMediaCount).isEqualTo(1) + val sendingState = awaitItem() + assertThat(sendingState.showAttachmentSourcePicker).isFalse() + assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending::class.java) + cancelAndIgnoreRemainingEvents() } } @@ -427,10 +404,11 @@ class MessageComposerPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(MessageComposerEvents.PickCameraAttachmentSource.Photo) - // Wait for the launched upload coroutine to run - runCurrent() - assertThat(room.sendMediaCount).isEqualTo(1) + initialState.eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) + val previewingState = awaitItem() + assertThat(previewingState.showAttachmentSourcePicker).isFalse() + assertThat(previewingState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java) + cancelAndIgnoreRemainingEvents() } } @@ -442,10 +420,10 @@ class MessageComposerPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(MessageComposerEvents.PickCameraAttachmentSource.Video) - // Wait for the launched upload coroutine to run - runCurrent() - assertThat(room.sendMediaCount).isEqualTo(1) + initialState.eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) + val previewingState = awaitItem() + assertThat(previewingState.showAttachmentSourcePicker).isFalse() + assertThat(previewingState.attachmentsState).isInstanceOf(AttachmentsState.Previewing::class.java) } } @@ -460,10 +438,11 @@ class MessageComposerPresenterTest { }.test { val initialState = awaitItem() initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) - + val sendingState = awaitItem() + assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending::class.java) + val finalState= awaitItem() + assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.None::class.java) snackbarDispatcher.snackbarMessage.test { - // Initial value is always null - skipItems(1) // Assert error message received assertThat(awaitItem()).isNotNull() } @@ -491,7 +470,8 @@ class MessageComposerPresenterTest { room, pickerProvider, featureFlagService, - mediaPreProcessor, + localMediaFactory, + MediaSender(mediaPreProcessor, room), snackbarDispatcher ) } From ced60c672e6bfa3d8dfd611fe9e395dad769fd81 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 24 May 2023 17:06:27 +0200 Subject: [PATCH 26/48] Media: add more tests --- .../media/local/AndroidLocalMediaFactory.kt | 11 +- .../impl/media/local/LocalMediaFactory.kt | 10 +- .../impl/media/viewer/MediaViewerPresenter.kt | 11 +- .../MessageComposerPresenter.kt | 28 ++--- .../messages/MessagesPresenterTest.kt | 2 +- .../AttachmentsPreviewPresenterTest.kt | 95 ++++++++++++++++ .../features/messages/fixtures/media.kt | 36 +++++++ .../messages/media/FakeLocalMediaFactory.kt | 37 +++++++ .../media/viewer/MediaViewerPresenterTest.kt | 102 ++++++++++++++++++ .../MessageComposerPresenterTest.kt | 9 +- .../matrix/impl/di/SessionMatrixModule.kt | 12 ++- .../android/libraries/matrix/test/TestData.kt | 5 + .../matrix/test/media/FakeMediaLoader.kt | 7 +- .../matrix/test/media/MediaSource.kt | 18 ++-- .../matrix/test/room/FakeMatrixRoom.kt | 24 +++-- 15 files changed, 354 insertions(+), 53 deletions(-) create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaFactory.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt => libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/MediaSource.kt (58%) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt index 8c7bef7cbe..ebc9bb9490 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt @@ -18,10 +18,12 @@ package io.element.android.features.messages.impl.media.local import android.content.Context import android.net.Uri +import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.matrix.api.media.MediaFile import javax.inject.Inject @ContributesBinding(AppScope::class) @@ -29,8 +31,13 @@ class AndroidLocalMediaFactory @Inject constructor( @ApplicationContext private val context: Context ) : LocalMediaFactory { - override fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia? { - if (uri == null) return null + override fun createFromMediaFile(mediaFile: MediaFile, mimeType: String?): LocalMedia { + val resolvedMimeType = mimeType ?: MimeTypes.OctetStream + val uri = mediaFile.path().toUri() + return LocalMedia(uri, resolvedMimeType) + } + + override fun createFromUri(uri: Uri, mimeType: String?): LocalMedia { val resolvedMimeType = mimeType ?: context.contentResolver.getType(uri) ?: MimeTypes.OctetStream return LocalMedia(uri, resolvedMimeType) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt index e7d2ea1ba6..04b2a54757 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt @@ -17,12 +17,20 @@ package io.element.android.features.messages.impl.media.local import android.net.Uri +import io.element.android.libraries.matrix.api.media.MediaFile interface LocalMediaFactory { + + /** + * This method will create a [LocalMedia] with the given [MediaFile] and [mimeType] + * + */ + fun createFromMediaFile(mediaFile: MediaFile, mimeType: String?): LocalMedia + /** * This method will create a [LocalMedia] with the given [uri] and [mimeType] * If the [mimeType] is null, it'll try to read it from the content. * */ - fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia? + fun createFromUri(uri: Uri, mimeType: String?): LocalMedia } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt index 7f672fcd3e..542436ed9f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt @@ -24,7 +24,6 @@ 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.AssistedFactory import dagger.assisted.AssistedInject @@ -32,7 +31,7 @@ import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.features.messages.impl.media.local.LocalMediaFactory import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -40,7 +39,7 @@ import kotlinx.coroutines.launch class MediaViewerPresenter @AssistedInject constructor( @Assisted private val inputs: MediaViewerNode.Inputs, private val localMediaFactory: LocalMediaFactory, - private val client: MatrixClient, + private val mediaLoader: MatrixMediaLoader, ) : Presenter { @AssistedFactory @@ -79,14 +78,12 @@ class MediaViewerPresenter @AssistedInject constructor( } private fun CoroutineScope.loadMedia(mediaFile: MutableState, localMedia: MutableState>) = launch { - mediaFile.value = null localMedia.value = Async.Loading() - client.mediaLoader.loadMediaFile(inputs.mediaSource, inputs.mimeType) + mediaLoader.loadMediaFile(inputs.mediaSource, inputs.mimeType) .onSuccess { mediaFile.value = it }.mapCatching { - val uri = it.path().toUri() - localMediaFactory.createFromUri(uri, inputs.mimeType)!! + localMediaFactory.createFromMediaFile(it, inputs.mimeType) }.onSuccess { localMedia.value = Async.Success(it) }.onFailure { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index a1c1e3e6d1..ae975f4ddb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -206,22 +206,22 @@ class MessageComposerPresenter @Inject constructor( mimeType: String? = null, compressIfPossible: Boolean = true, ) { + if (uri == null) { + attachmentsState.value = AttachmentsState.None + return + } val localMedia = localMediaFactory.createFromUri(uri, mimeType) - attachmentsState.value = if (localMedia == null) { - AttachmentsState.None + val mediaAttachment = Attachment.Media(localMedia, compressIfPossible) + val isPreviewable = when { + MimeTypes.isImage(localMedia.mimeType) -> true + MimeTypes.isVideo(localMedia.mimeType) -> true + MimeTypes.isAudio(localMedia.mimeType) -> true + else -> false + } + attachmentsState.value = if (isPreviewable) { + AttachmentsState.Previewing(persistentListOf(mediaAttachment)) } else { - val mediaAttachment = Attachment.Media(localMedia, compressIfPossible) - val isPreviewable = when { - MimeTypes.isImage(localMedia.mimeType) -> true - MimeTypes.isVideo(localMedia.mimeType) -> true - MimeTypes.isAudio(localMedia.mimeType) -> true - else -> false - } - if (isPreviewable) { - AttachmentsState.Previewing(persistentListOf(mediaAttachment)) - } else { - AttachmentsState.Sending(persistentListOf(mediaAttachment)) - } + AttachmentsState.Sending(persistentListOf(mediaAttachment)) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 93db073f78..f2691ca1a9 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -26,7 +26,7 @@ import io.element.android.features.messages.impl.MessagesEvents import io.element.android.features.messages.impl.MessagesPresenter import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction -import io.element.android.features.messages.impl.media.local.FakeLocalMediaFactory +import io.element.android.features.messages.media.FakeLocalMediaFactory import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.networkmonitor.test.FakeNetworkMonitor diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt new file mode 100644 index 0000000000..0b16a254a1 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt @@ -0,0 +1,95 @@ +/* + * 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.attachments + +import androidx.media3.common.MimeTypes +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.fixtures.aLocalMedia +import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewEvents +import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewPresenter +import io.element.android.features.messages.impl.media.local.LocalMedia +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.mediaupload.api.MediaPreProcessor +import io.element.android.libraries.mediaupload.api.MediaSender +import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class AttachmentsPreviewPresenterTest { + + private val mediaPreProcessor = FakeMediaPreProcessor() + + @Test + fun `present - send media success scenario`() = runTest { + val room = FakeMatrixRoom() + val presenter = anAttachmentsPreviewPresenter(room = room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.sendActionState).isEqualTo(Async.Uninitialized) + initialState.eventSink(AttachmentsPreviewEvents.SendAttachment) + val loadingState = awaitItem() + assertThat(loadingState.sendActionState).isEqualTo(Async.Loading()) + testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS) + val successState = awaitItem() + assertThat(successState.sendActionState).isEqualTo(Async.Success(Unit)) + assertThat(room.sendMediaCount).isEqualTo(1) + } + } + + @Test + fun `present - send media failure scenario`() = runTest { + val room = FakeMatrixRoom() + val failure = MediaPreProcessor.Failure(null) + room.givenSendMediaResult(Result.failure(failure)) + val presenter = anAttachmentsPreviewPresenter(room = room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.sendActionState).isEqualTo(Async.Uninitialized) + initialState.eventSink(AttachmentsPreviewEvents.SendAttachment) + val loadingState = awaitItem() + assertThat(loadingState.sendActionState).isEqualTo(Async.Loading()) + testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS) + val failureState = awaitItem() + assertThat(failureState.sendActionState).isEqualTo(Async.Failure(failure)) + assertThat(room.sendMediaCount).isEqualTo(0) + failureState.eventSink(AttachmentsPreviewEvents.ClearSendState) + val clearedState = awaitItem() + assertThat(clearedState.sendActionState).isEqualTo(Async.Uninitialized) + } + } + + private fun anAttachmentsPreviewPresenter( + localMedia: LocalMedia = aLocalMedia(mimeType = MimeTypes.IMAGE_JPEG), + room: MatrixRoom = FakeMatrixRoom() + ): AttachmentsPreviewPresenter { + return AttachmentsPreviewPresenter( + attachment = Attachment.Media(localMedia, compressIfPossible = false), + mediaSender = MediaSender(mediaPreProcessor, room) + ) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt new file mode 100644 index 0000000000..5b63bc1e68 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt @@ -0,0 +1,36 @@ +/* + * 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.fixtures + +import android.net.Uri +import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.media.local.LocalMedia +import io.mockk.mockk + +fun aLocalMedia( + uri: Uri = mockk("localMediaUri"), + mimeType: String +) = LocalMedia( + uri = uri, + mimeType = mimeType +) + +fun aMediaAttachment(localMedia: LocalMedia, compressIfPossible: Boolean = true) = Attachment.Media( + localMedia = localMedia, + compressIfPossible = compressIfPossible, +) + diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaFactory.kt new file mode 100644 index 0000000000..b75eec284a --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaFactory.kt @@ -0,0 +1,37 @@ +/* + * 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.media + +import android.net.Uri +import io.element.android.features.messages.fixtures.aLocalMedia +import io.element.android.features.messages.impl.media.local.LocalMedia +import io.element.android.features.messages.impl.media.local.LocalMediaFactory +import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.matrix.api.media.MediaFile + +class FakeLocalMediaFactory() : LocalMediaFactory { + + var fallbackMimeType: String = MimeTypes.OctetStream + + override fun createFromMediaFile(mediaFile: MediaFile, mimeType: String?): LocalMedia { + return aLocalMedia(mimeType = mimeType ?: fallbackMimeType) + } + + override fun createFromUri(uri: Uri, mimeType: String?): LocalMedia { + return aLocalMedia(uri, mimeType ?: fallbackMimeType) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt new file mode 100644 index 0000000000..a828356586 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt @@ -0,0 +1,102 @@ +/* + * 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.media.viewer + +import androidx.media3.common.MimeTypes +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.features.messages.impl.media.viewer.MediaViewerEvents +import io.element.android.features.messages.impl.media.viewer.MediaViewerNode +import io.element.android.features.messages.impl.media.viewer.MediaViewerPresenter +import io.element.android.features.messages.media.FakeLocalMediaFactory +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS +import io.element.android.libraries.matrix.test.media.FakeMediaLoader +import io.element.android.libraries.matrix.test.media.aMediaSource +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private const val TESTED_MIME_TYPE = MimeTypes.IMAGE_JPEG +private const val TESTED_MEDIA_NAME = "MediaName" + +class MediaViewerPresenterTest { + + private val localMediaFactory = FakeLocalMediaFactory() + private val mediaLoader = FakeMediaLoader() + + @Test + fun `present - download media success scenario`() = runTest { + val presenter = aMediaViewerPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.downloadedMedia).isEqualTo(Async.Uninitialized) + assertThat(initialState.name).isEqualTo(TESTED_MEDIA_NAME) + val loadingState = awaitItem() + assertThat(loadingState.downloadedMedia).isInstanceOf(Async.Loading::class.java) + testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS) + val successState = awaitItem() + val successData = successState.downloadedMedia.dataOrNull() + assertThat(successState.downloadedMedia).isInstanceOf(Async.Success::class.java) + assertThat(successData).isNotNull() + } + } + + @Test + fun `present - download media failure then retry with success scenario`() = runTest { + val presenter = aMediaViewerPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + mediaLoader.shouldFail = true + val initialState = awaitItem() + assertThat(initialState.downloadedMedia).isEqualTo(Async.Uninitialized) + assertThat(initialState.name).isEqualTo(TESTED_MEDIA_NAME) + val loadingState = awaitItem() + assertThat(loadingState.downloadedMedia).isInstanceOf(Async.Loading::class.java) + testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS) + val failureState = awaitItem() + assertThat(failureState.downloadedMedia).isInstanceOf(Async.Failure::class.java) + mediaLoader.shouldFail = false + failureState.eventSink(MediaViewerEvents.RetryLoading) + //There is one recomposition because of the retry mechanism + skipItems(1) + val retryLoadingState = awaitItem() + assertThat(retryLoadingState.downloadedMedia).isInstanceOf(Async.Loading::class.java) + testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS) + val successState = awaitItem() + val successData = successState.downloadedMedia.dataOrNull() + assertThat(successState.downloadedMedia).isInstanceOf(Async.Success::class.java) + assertThat(successData).isNotNull() + } + } + + private fun aMediaViewerPresenter(mimeType: String = TESTED_MIME_TYPE): MediaViewerPresenter { + return MediaViewerPresenter( + inputs = MediaViewerNode.Inputs( + name = TESTED_MEDIA_NAME, + mediaSource = aMediaSource(), + mimeType = mimeType + ), + localMediaFactory = localMediaFactory, + mediaLoader = mediaLoader + ) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 1736672c98..40ca7def0a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -21,7 +21,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.messages.impl.media.local.FakeLocalMediaFactory +import io.element.android.features.messages.media.FakeLocalMediaFactory import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter @@ -52,7 +52,6 @@ import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.textcomposer.MessageComposerMode import io.mockk.mockk import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import java.io.File @@ -392,7 +391,9 @@ class MessageComposerPresenterTest { val sendingState = awaitItem() assertThat(sendingState.showAttachmentSourcePicker).isFalse() assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending::class.java) - cancelAndIgnoreRemainingEvents() + val sentState = awaitItem() + assertThat(sentState.attachmentsState).isEqualTo(AttachmentsState.None) + assertThat(room.sendMediaCount).isEqualTo(1) } } @@ -440,7 +441,7 @@ class MessageComposerPresenterTest { initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) val sendingState = awaitItem() assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending::class.java) - val finalState= awaitItem() + val finalState = awaitItem() assertThat(finalState.attachmentsState).isInstanceOf(AttachmentsState.None::class.java) snackbarDispatcher.snackbarMessage.test { // Assert error message received diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt index b49050cdc2..104a204164 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt @@ -22,16 +22,16 @@ import dagger.Provides import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.verification.SessionVerificationService @Module @ContributesTo(SessionScope::class) object SessionMatrixModule { @Provides @SingleIn(SessionScope::class) - fun providesRustSessionVerificationService(matrixClient: MatrixClient): SessionVerificationService { + fun providesSessionVerificationService(matrixClient: MatrixClient): SessionVerificationService { return matrixClient.sessionVerificationService() } @@ -40,4 +40,10 @@ object SessionMatrixModule { fun provideRoomMembershipObserver(matrixClient: MatrixClient): RoomMembershipObserver { return matrixClient.roomMembershipObserver() } + + @Provides + @SingleIn(SessionScope::class) + fun provideMediaLoader(matrixClient: MatrixClient): MatrixMediaLoader { + return matrixClient.mediaLoader + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index 60e8c0caa0..56a2e3cd85 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -23,6 +23,8 @@ 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.ThreadId 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_PASSWORD = "password" @@ -52,6 +54,9 @@ val A_HOMESERVER = MatrixHomeServerDetails(A_HOMESERVER_URL, true, null) const val AN_AVATAR_URL = "mxc://data" const val A_FAILURE_REASON = "There has been a failure" + +const val FAKE_DELAY_IN_MS = 100L + val A_THROWABLE = Throwable(A_FAILURE_REASON) val AN_EXCEPTION = Exception(A_FAILURE_REASON) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt index ebc823f6d1..266c81c605 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt @@ -17,14 +17,17 @@ package io.element.android.libraries.matrix.test.media import io.element.android.libraries.matrix.api.media.MatrixMediaLoader -import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaFile +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS +import kotlinx.coroutines.delay class FakeMediaLoader : MatrixMediaLoader { var shouldFail = false override suspend fun loadMediaContent(source: MediaSource): Result { + delay(FAKE_DELAY_IN_MS) return if (shouldFail) { Result.failure(RuntimeException()) } else { @@ -33,6 +36,7 @@ class FakeMediaLoader : MatrixMediaLoader { } override suspend fun loadMediaThumbnail(source: MediaSource, width: Long, height: Long): Result { + delay(FAKE_DELAY_IN_MS) return if (shouldFail) { Result.failure(RuntimeException()) } else { @@ -41,6 +45,7 @@ class FakeMediaLoader : MatrixMediaLoader { } override suspend fun loadMediaFile(source: MediaSource, mimeType: String?): Result { + delay(FAKE_DELAY_IN_MS) return if (shouldFail) { Result.failure(RuntimeException()) } else { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/MediaSource.kt similarity index 58% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt rename to libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/MediaSource.kt index 83915dd89d..4a0e9005d2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/FakeLocalMediaFactory.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/MediaSource.kt @@ -14,17 +14,11 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.media.local +package io.element.android.libraries.matrix.test.media -import android.net.Uri -import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.matrix.api.media.MediaSource -class FakeLocalMediaFactory() : LocalMediaFactory { - - var fallbackMimeType: String = MimeTypes.OctetStream - - override fun createFromUri(uri: Uri?, mimeType: String?): LocalMedia? { - if (uri == null) return null - return LocalMedia(uri, mimeType ?: fallbackMimeType) - } -} +fun aMediaSource(url: String = "") = MediaSource( + url = url, + json = null +) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index a790812137..88d23d5b91 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -94,7 +95,7 @@ class FakeMatrixRoom( } override suspend fun sendMessage(message: String): Result { - delay(100) + delay(FAKE_DELAY_IN_MS) return Result.success(Unit) } @@ -103,7 +104,7 @@ class FakeMatrixRoom( override suspend fun editMessage(originalEventId: EventId, message: String): Result { editMessageParameter = message - delay(100) + delay(FAKE_DELAY_IN_MS) return Result.success(Unit) } @@ -112,7 +113,7 @@ class FakeMatrixRoom( override suspend fun replyMessage(eventId: EventId, message: String): Result { replyMessageParameter = message - delay(100) + delay(FAKE_DELAY_IN_MS) return Result.success(Unit) } @@ -121,7 +122,7 @@ class FakeMatrixRoom( override suspend fun redactEvent(eventId: EventId, reason: String?): Result { redactEventEventIdParam = eventId - delay(100) + delay(FAKE_DELAY_IN_MS) return Result.success(Unit) } @@ -136,13 +137,20 @@ class FakeMatrixRoom( return rejectInviteResult } - override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result = sendMediaResult.also { sendMediaCount++ } + override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result = fakeSendMedia() - override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result = sendMediaResult.also { sendMediaCount++ } + override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result = fakeSendMedia() - override suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result = sendMediaResult.also { sendMediaCount++ } + override suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result = fakeSendMedia() - override suspend fun sendFile(file: File, fileInfo: FileInfo): Result = sendMediaResult.also { sendMediaCount++ } + override suspend fun sendFile(file: File, fileInfo: FileInfo): Result = fakeSendMedia() + + private suspend fun fakeSendMedia(): Result { + delay(FAKE_DELAY_IN_MS) + return sendMediaResult.onSuccess { + sendMediaCount++ + } + } override fun close() = Unit From 75f85a97b7e0ee2a99a82c90267aa92dc80fa040 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 24 May 2023 17:30:09 +0200 Subject: [PATCH 27/48] Media: makes sure system ui get back to normal when leaving preview/viewer --- .../preview/AttachmentsPreviewNode.kt | 4 +- .../impl/media/viewer/MediaViewerNode.kt | 4 +- .../designsystem/theme/ElementTheme.kt | 44 +++++++++++++++---- .../designsystem/theme/components/Divider.kt | 1 - .../designsystem/theme/components/Surface.kt | 2 - 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt index a31368896e..370033774c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewNode.kt @@ -27,7 +27,7 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.ForcedDarkElementTheme import io.element.android.libraries.di.RoomScope @ContributesNode(RoomScope::class) @@ -45,7 +45,7 @@ class AttachmentsPreviewNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - ElementTheme(darkTheme = true) { + ForcedDarkElementTheme { val state = presenter.present() AttachmentsPreviewView( state = state, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt index 2ebdba3b1c..7321eff416 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt @@ -26,7 +26,7 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.ForcedDarkElementTheme import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.media.MediaSource @@ -49,7 +49,7 @@ class MediaViewerNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - ElementTheme(darkTheme = true) { + ForcedDarkElementTheme { val state = presenter.present() MediaViewerView( state = state, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt index 883346571a..83de1b63a1 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementTheme.kt @@ -24,12 +24,14 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import com.google.accompanist.systemuicontroller.SystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController /** @@ -55,7 +57,6 @@ fun ElementTheme( content: @Composable () -> Unit, ) { val systemUiController = rememberSystemUiController() - val useDarkIcons = !darkTheme val currentColor = remember(darkTheme) { colors.copy() }.apply { updateColorsFrom(colors) } @@ -68,13 +69,7 @@ fun ElementTheme( else -> materialLightColors } SideEffect { - systemUiController.setStatusBarColor( - color = colorScheme.background - ) - systemUiController.setSystemBarsColor( - color = Color.Transparent, - darkIcons = useDarkIcons - ) + systemUiController.applyTheme(colorScheme = colorScheme, darkTheme = darkTheme) } CompositionLocalProvider( LocalColors provides currentColor, @@ -86,3 +81,36 @@ fun ElementTheme( ) } } + +/** + * Can be used to force a composable in dark theme. + * It will automatically change the system ui colors back to normal when leaving the composition. + */ +@Composable +fun ForcedDarkElementTheme( + content: @Composable () -> Unit, +) { + val systemUiController = rememberSystemUiController() + val colorScheme = MaterialTheme.colorScheme + val wasDarkTheme = !ElementTheme.colors.isLight + DisposableEffect(Unit) { + onDispose { + systemUiController.applyTheme(colorScheme, wasDarkTheme) + } + } + ElementTheme(darkTheme = true, content = content) +} + +private fun SystemUiController.applyTheme( + colorScheme: ColorScheme, + darkTheme: Boolean, +) { + val useDarkIcons = !darkTheme + setStatusBarColor( + color = colorScheme.background + ) + setSystemBarsColor( + color = Color.Transparent, + darkIcons = useDarkIcons + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Divider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Divider.kt index 41c987d7b2..692e89f8e8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Divider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Divider.kt @@ -17,7 +17,6 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.padding import androidx.compose.material3.DividerDefaults import androidx.compose.runtime.Composable diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Surface.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Surface.kt index f4496ca87f..db7ab7fc08 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Surface.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Surface.kt @@ -29,8 +29,6 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementThemedPreview @Composable From 92e19c3dd99f8f34362459223807603683b38a4d Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 24 May 2023 22:19:18 +0200 Subject: [PATCH 28/48] Media: improve a bit the viewers --- .../preview/AttachmentsPreviewStateProvider.kt | 3 +-- .../preview/AttachmentsPreviewView.kt | 16 ++++++++-------- .../media/local/AndroidLocalMediaFactory.kt | 14 +++++++++++--- .../messages/impl/media/local/LocalMedia.kt | 2 ++ .../messages/impl/media/local/LocalMediaView.kt | 11 +++++++++++ .../impl/media/local/UriToFileMapper.kt | 3 ++- .../media/viewer/MediaViewerStateProvider.kt | 17 ++++++++++++++--- .../impl/media/viewer/MediaViewerView.kt | 9 +-------- .../AttachmentsPreviewPresenterTest.kt | 2 +- .../android/features/messages/fixtures/media.kt | 8 ++++++-- .../libraries/androidutils/file/Context.kt | 14 ++++++++++++++ 11 files changed, 71 insertions(+), 28 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt index 220b4554dc..26565a226a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt @@ -29,13 +29,12 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider = Async.Uninitialized) = AttachmentsPreviewState( attachment = Attachment.Media( - localMedia = LocalMedia("".toUri(), mimeType = MimeTypes.OctetStream), + localMedia = LocalMedia("path".toUri(), MimeTypes.Jpeg, "an image", 1000L), compressIfPossible = true ), sendActionState = sendActionState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt index 3ff750f101..28c78da1be 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt @@ -79,13 +79,13 @@ fun AttachmentsPreviewView( onSendClicked = ::postSendAttachment, onDismiss = onDismiss ) - AttachmentSendStateView( - sendActionState = state.sendActionState, - onRetryClicked = ::postSendAttachment, - onRetryDismissed = ::postClearSendState - ) } } + AttachmentSendStateView( + sendActionState = state.sendActionState, + onRetryClicked = ::postSendAttachment, + onRetryDismissed = ::postClearSendState + ) } @Composable @@ -106,7 +106,6 @@ private fun AttachmentSendStateView( onRetry = onRetryClicked ) } - else -> Unit } } @@ -115,10 +114,11 @@ private fun AttachmentSendStateView( private fun AttachmentPreviewContent( attachment: Attachment, onSendClicked: () -> Unit, - onDismiss: () -> Unit + onDismiss: () -> Unit, + modifier: Modifier = Modifier, ) { Column( - modifier = Modifier + modifier = modifier .fillMaxSize() .padding(top = 24.dp) ) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt index ebc9bb9490..bc2f1a066c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt @@ -20,6 +20,8 @@ import android.content.Context import android.net.Uri import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.androidutils.file.getFileName +import io.element.android.libraries.androidutils.file.getFileSize import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext @@ -32,13 +34,19 @@ class AndroidLocalMediaFactory @Inject constructor( ) : LocalMediaFactory { override fun createFromMediaFile(mediaFile: MediaFile, mimeType: String?): LocalMedia { - val resolvedMimeType = mimeType ?: MimeTypes.OctetStream val uri = mediaFile.path().toUri() - return LocalMedia(uri, resolvedMimeType) + return createFromUri(uri, mimeType) } override fun createFromUri(uri: Uri, mimeType: String?): LocalMedia { val resolvedMimeType = mimeType ?: context.contentResolver.getType(uri) ?: MimeTypes.OctetStream - return LocalMedia(uri, resolvedMimeType) + val fileName = context.getFileName(uri) + val fileSize = context.getFileSize(uri) + return LocalMedia( + uri = uri, + mimeType = resolvedMimeType, + name = fileName, + size = fileSize + ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt index 016fa382c3..41ff1025ff 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt @@ -25,6 +25,8 @@ import kotlinx.parcelize.Parcelize data class LocalMedia( val uri: Uri, val mimeType: String, + val name: String?, + val size: Long, ) : Parcelable { /** diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt index f814dd6662..2365ac317f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.media.local import android.annotation.SuppressLint import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -26,6 +27,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.media3.common.MediaItem @@ -34,6 +37,7 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView +import io.element.android.libraries.designsystem.R import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import me.saket.telephoto.zoomable.ZoomSpec import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage @@ -64,6 +68,13 @@ private fun MediaImageView( localMedia: LocalMedia, modifier: Modifier = Modifier, ) { + if (LocalInspectionMode.current) { + Image( + painter = painterResource(id = R.drawable.sample_background), + modifier = modifier.fillMaxSize(), + contentDescription = null, + ) + } val zoomableState = rememberZoomableState( zoomSpec = ZoomSpec(maxZoomFactor = 3f) ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/UriToFileMapper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/UriToFileMapper.kt index 417219c60e..d27b667883 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/UriToFileMapper.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/UriToFileMapper.kt @@ -40,7 +40,8 @@ object UriToFileMapper { } private fun isApplicable(data: Uri): Boolean { - return data.scheme.let { it == null || it == ContentResolver.SCHEME_FILE } && + return !isAssetUri(data) && + data.scheme.let { it == null || it == ContentResolver.SCHEME_FILE } && data.path.orEmpty().startsWith('/') && data.firstPathSegment != null } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt index 66c41a0eba..45ac530b8e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt @@ -16,19 +16,30 @@ package io.element.android.features.messages.impl.media.viewer +import android.net.Uri import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.media3.common.MimeTypes +import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.libraries.architecture.Async open class MediaViewerStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMediaViewerState(), - // Add other states here + aMediaViewerState(Async.Loading()), + aMediaViewerState(Async.Failure(IllegalStateException())), + aMediaViewerState( + Async.Success( + LocalMedia( + Uri.EMPTY, MimeTypes.IMAGE_JPEG, "a file", 100L + ) + ), + ) ) } -fun aMediaViewerState() = MediaViewerState( +fun aMediaViewerState(downloadedMedia: Async = Async.Uninitialized) = MediaViewerState( name = "A media", - downloadedMedia = Async.Uninitialized, + downloadedMedia = downloadedMedia, eventSink = {} ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt index 9dbcaf5559..b5ef22b596 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -63,9 +63,7 @@ fun MediaViewerView( when (state.downloadedMedia) { is Async.Success -> LocalMediaView(state.downloadedMedia.state) is Async.Failure -> ErrorView(stringResource(id = StringR.string.error_unknown), ::onRetry) - else -> CircularProgressIndicator( - strokeWidth = 2.dp, - ) + else -> CircularProgressIndicator() } } } @@ -92,11 +90,6 @@ private fun ErrorView( } } -@Preview -@Composable -fun MediaViewerViewLightPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = - ElementPreviewLight { ContentToPreview(state) } - @Preview @Composable fun MediaViewerViewDarkPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt index 0b16a254a1..27657bb560 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt @@ -84,7 +84,7 @@ class AttachmentsPreviewPresenterTest { } private fun anAttachmentsPreviewPresenter( - localMedia: LocalMedia = aLocalMedia(mimeType = MimeTypes.IMAGE_JPEG), + localMedia: LocalMedia = aLocalMedia(MimeTypes.IMAGE_JPEG), room: MatrixRoom = FakeMatrixRoom() ): AttachmentsPreviewPresenter { return AttachmentsPreviewPresenter( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt index 5b63bc1e68..df4aa1f258 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt @@ -22,11 +22,15 @@ import io.element.android.features.messages.impl.media.local.LocalMedia import io.mockk.mockk fun aLocalMedia( + mimeType: String, uri: Uri = mockk("localMediaUri"), - mimeType: String + name: String = "a media", + size: Long = 1000, ) = LocalMedia( uri = uri, - mimeType = mimeType + mimeType = mimeType, + name = name, + size = size, ) fun aMediaAttachment(localMedia: LocalMedia, compressIfPossible: Boolean = true) = Attachment.Media( diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt index a5fab7545c..6bf784b100 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/Context.kt @@ -27,6 +27,20 @@ fun Context.getFileName(uri: Uri): String? = when (uri.scheme) { else -> uri.path?.let(::File)?.name } +fun Context.getFileSize(uri: Uri): Long { + return when (uri.scheme) { + ContentResolver.SCHEME_CONTENT -> getContentFileSize(uri) + else -> uri.path?.let(::File)?.length() + } ?: 0 +} + +private fun Context.getContentFileSize(uri: Uri): Long? = runCatching { + contentResolver.query(uri, null, null, null, null)?.use { cursor -> + cursor.moveToFirst() + return@use cursor.getColumnIndexOrThrow(OpenableColumns.SIZE).let(cursor::getLong) + } +}.getOrNull() + private fun Context.getContentFileName(uri: Uri): String? = runCatching { contentResolver.query(uri, null, null, null, null)?.use { cursor -> cursor.moveToFirst() From 3e01a2f31760b8076fb055c1cc44e22797ecc29b Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 24 May 2023 22:23:38 +0200 Subject: [PATCH 29/48] Media: some code clean-up --- .../impl/media/local/LocalMediaFactory.kt | 2 +- .../impl/media/local/LocalMediaView.kt | 21 ++++++++++--------- .../AttachmentsPreviewPresenterTest.kt | 2 +- .../features/messages/fixtures/media.kt | 3 ++- .../messages/media/FakeLocalMediaFactory.kt | 2 +- .../libraries/matrix/api/media/MediaSource.kt | 4 ++-- .../{MediaHandle.kt => RustMediaFile.kt} | 0 7 files changed, 18 insertions(+), 16 deletions(-) rename libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/{MediaHandle.kt => RustMediaFile.kt} (100%) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt index 04b2a54757..581461de7f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt @@ -22,7 +22,7 @@ import io.element.android.libraries.matrix.api.media.MediaFile interface LocalMediaFactory { /** - * This method will create a [LocalMedia] with the given [MediaFile] and [mimeType] + * This method will create a [LocalMedia] with the given [MediaFile] and [mimeType]. * */ fun createFromMediaFile(mediaFile: MediaFile, mimeType: String?): LocalMedia diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt index 2365ac317f..1f3a67a8ec 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt @@ -74,17 +74,18 @@ private fun MediaImageView( modifier = modifier.fillMaxSize(), contentDescription = null, ) + } else { + val zoomableState = rememberZoomableState( + zoomSpec = ZoomSpec(maxZoomFactor = 3f) + ) + ZoomableAsyncImage( + modifier = modifier.fillMaxSize(), + state = rememberZoomableImageState(zoomableState), + model = localMedia.model, + contentDescription = "Image", + contentScale = ContentScale.Fit, + ) } - val zoomableState = rememberZoomableState( - zoomSpec = ZoomSpec(maxZoomFactor = 3f) - ) - ZoomableAsyncImage( - modifier = modifier.fillMaxSize(), - state = rememberZoomableImageState(zoomableState), - model = localMedia.model, - contentDescription = "Image", - contentScale = ContentScale.Fit, - ) } @UnstableApi diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt index 27657bb560..0b16a254a1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt @@ -84,7 +84,7 @@ class AttachmentsPreviewPresenterTest { } private fun anAttachmentsPreviewPresenter( - localMedia: LocalMedia = aLocalMedia(MimeTypes.IMAGE_JPEG), + localMedia: LocalMedia = aLocalMedia(mimeType = MimeTypes.IMAGE_JPEG), room: MatrixRoom = FakeMatrixRoom() ): AttachmentsPreviewPresenter { return AttachmentsPreviewPresenter( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt index df4aa1f258..ce5847f559 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt @@ -17,13 +17,14 @@ package io.element.android.features.messages.fixtures import android.net.Uri +import androidx.media3.common.MimeTypes import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.media.local.LocalMedia import io.mockk.mockk fun aLocalMedia( - mimeType: String, uri: Uri = mockk("localMediaUri"), + mimeType: String = MimeTypes.IMAGE_JPEG, name: String = "a media", size: Long = 1000, ) = LocalMedia( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaFactory.kt index b75eec284a..0382941d87 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaFactory.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaFactory.kt @@ -23,7 +23,7 @@ import io.element.android.features.messages.impl.media.local.LocalMediaFactory import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.media.MediaFile -class FakeLocalMediaFactory() : LocalMediaFactory { +class FakeLocalMediaFactory : LocalMediaFactory { var fallbackMimeType: String = MimeTypes.OctetStream diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaSource.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaSource.kt index fe5217c2c2..170137302b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaSource.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaSource.kt @@ -22,11 +22,11 @@ import kotlinx.parcelize.Parcelize @Parcelize data class MediaSource( /** - * Url of the media + * Url of the media. */ val url: String, /** - * This is used to hold data for encrypted media + * This is used to hold data for encrypted media. */ val json: String? = null, ) : Parcelable diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaHandle.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaFile.kt similarity index 100% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaHandle.kt rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaFile.kt From 48389ccd2628c794b62c36bbd6d689c76b4a04d4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 May 2023 15:28:37 +0200 Subject: [PATCH 30/48] Media: improve BlurHashAsyncImage --- .../components/blurhash/BlurHashAsyncImage.kt | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt index 7f65a54795..f3f2addfa7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt @@ -16,17 +16,24 @@ package io.element.android.features.messages.impl.timeline.components.blurhash -import android.graphics.Bitmap +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale -import coil.compose.SubcomposeAsyncImage +import coil.compose.AsyncImage import com.vanniktech.blurhash.BlurHash @Composable @@ -37,19 +44,29 @@ fun BlurHashAsyncImage( contentScale: ContentScale = ContentScale.Fit, contentDescription: String? = null, ) { - SubcomposeAsyncImage( - model = model, + var isLoading by rememberSaveable(model) { mutableStateOf(true) } + Box( modifier = modifier, - contentScale = contentScale, - contentDescription = contentDescription, - loading = { + contentAlignment = Alignment.Center, + ) { + AsyncImage( + model = model, + contentScale = contentScale, + contentDescription = contentDescription, + onSuccess = { isLoading = false } + ) + AnimatedVisibility( + visible = isLoading, + enter = fadeIn(), + exit = fadeOut(), + ) { BlurHashImage( blurHash = blurHash, + contentDescription = contentDescription, contentScale = ContentScale.FillBounds, - contentDescription = "Loading placeholder" ) - }, - ) + } + } } @Composable @@ -60,12 +77,13 @@ fun BlurHashImage( contentScale: ContentScale = ContentScale.Fit, ) { if (blurHash == null) return - val bitmapState = remember { - mutableStateOf(null) + val bitmapState = remember(blurHash) { + mutableStateOf( + // Build a small blurhash image so that it's fast + BlurHash.decode(blurHash, 10, 10) + ) } DisposableEffect(blurHash) { - // Build a small blurhash image so that it's fast - bitmapState.value = BlurHash.decode(blurHash, 10, 10) onDispose { bitmapState.value?.recycle() } From 6eaee151c525611f16842f060d9d6351a6c35141 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 May 2023 15:29:02 +0200 Subject: [PATCH 31/48] Media: update androidx.media --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 96a384d397..3e77870160 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ recyclerview = "1.3.0" lifecycle = "2.6.1" activity = "1.7.1" startup = "1.1.1" -media3 = "1.0.1" +media3 = "1.0.2" # Compose compose_bom = "2023.05.01" From f7b45d78233e49ecb6aeabd2c50efcad3401cc75 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 May 2023 15:47:08 +0200 Subject: [PATCH 32/48] Media: more cleanup --- .../components/event/TimelineItemAspectRatioBox.kt | 3 ++- .../timeline/components/event/TimelineItemImageView.kt | 4 ++-- .../timeline/components/event/TimelineItemVideoView.kt | 9 +++++---- .../model/event/TimelineItemImageContentProvider.kt | 5 +++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt index c3618bb3d2..33fbeaa8ce 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt @@ -32,11 +32,12 @@ fun TimelineItemAspectRatioBox( modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit, ) { + // TODO should probably be moved to an ElementTheme.dimensions val maxHeight = min(300, height ?: 0) Box( modifier = modifier .heightIn(max = maxHeight.dp) - .aspectRatio(aspectRatio), + .aspectRatio(aspectRatio, matchHeightConstraintsFirst = true), content = content ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index f1dd1625f0..f2cb2b2b92 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -40,9 +40,9 @@ fun TimelineItemImageView( modifier = modifier ) { BlurHashAsyncImage( - modifier = Modifier.fillMaxSize(), - blurHash = content.blurhash, model = MediaRequestData(content.mediaSource, MediaRequestData.Kind.Content), + blurHash = content.blurhash, + modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Fit, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index c56c9b18fb..1eeb5c6216 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -18,13 +18,14 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.foundation.Image import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayCircle import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.messages.impl.timeline.components.blurhash.BlurHashAsyncImage @@ -46,13 +47,13 @@ fun TimelineItemVideoView( modifier = modifier ) { BlurHashAsyncImage( - modifier = Modifier.fillMaxSize(), - blurHash = content.blurhash, model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.Content), + blurHash = content.blurhash, + modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Fit, ) Image( - painterResource(id = androidx.media3.ui.R.drawable.exo_ic_play_circle_filled), + Icons.Default.PlayCircle, contentDescription = null, colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground), modifier = Modifier.align(Alignment.Center), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt index 0bedfd8991..6324789d8f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.media3.common.MimeTypes import io.element.android.libraries.matrix.api.media.MediaSource open class TimelineItemImageContentProvider : PreviewParameterProvider { @@ -33,7 +34,7 @@ fun aTimelineItemImageContent() = TimelineItemImageContent( mediaSource = MediaSource(""), blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr", aspectRatio = 0.5f, - mimeType = "null", - height = null, + mimeType = MimeTypes.IMAGE_JPEG, + height = 300, width = null ) From 1fe14ef6858d40a7c9bd2417102f88a4a4ac8fe0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 May 2023 15:58:08 +0200 Subject: [PATCH 33/48] Media: update screenshots --- .../event/TimelineItemAspectRatioBox.kt | 3 +++ .../components/event/TimelineItemVideoView.kt | 16 ++++++++-------- .../event/TimelineItemVideoContentProvider.kt | 6 +++--- ..._AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...AvatarLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...wViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...wViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...wViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...rViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...rViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...rViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...rViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 +++ ...ViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png} | 0 ...iewLightPreview_0_null_0,NEXUS_5,1.0,en].png} | 0 ...eViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...eViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...eViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...eViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...oViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...oViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...oViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...ViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...uttonDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...uttonDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...uttonDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...uttonDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ttonLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ttonLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ttonLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ttonLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...onsViewDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...nsViewLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...eViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...eViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 2 +- ...eViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...eViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...eViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...eViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...ViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ViewLightPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...ViewLightPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...sViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...sViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...sViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...sViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 --- ...ViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 2 +- ...ViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 --- ...gScreenDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ScreenLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...tViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 2 +- ...ViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- 62 files changed, 136 insertions(+), 94 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.textcomposer_null_DefaultGroup_MessageComposerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.messagecomposer_null_DefaultGroup_MessageComposerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.textcomposer_null_DefaultGroup_MessageComposerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.messagecomposer_null_DefaultGroup_MessageComposerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png} (100%) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_2,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt index 33fbeaa8ce..043019cc7f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.heightIn import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import kotlin.math.min @@ -30,6 +31,7 @@ fun TimelineItemAspectRatioBox( height: Int?, aspectRatio: Float, modifier: Modifier = Modifier, + contentAlignment: Alignment = Alignment.TopStart, content: @Composable BoxScope.() -> Unit, ) { // TODO should probably be moved to an ElementTheme.dimensions @@ -38,6 +40,7 @@ fun TimelineItemAspectRatioBox( modifier = modifier .heightIn(max = maxHeight.dp) .aspectRatio(aspectRatio, matchHeightConstraintsFirst = true), + contentAlignment = contentAlignment, content = content ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index 1eeb5c6216..56c55e4ef2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -29,9 +29,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.messages.impl.timeline.components.blurhash.BlurHashAsyncImage -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContentProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.matrix.ui.media.MediaRequestData @@ -44,7 +43,8 @@ fun TimelineItemVideoView( TimelineItemAspectRatioBox( height = content.height, aspectRatio = content.aspectRatio, - modifier = modifier + modifier = modifier, + contentAlignment = Alignment.Center, ) { BlurHashAsyncImage( model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.Content), @@ -54,7 +54,7 @@ fun TimelineItemVideoView( ) Image( Icons.Default.PlayCircle, - contentDescription = null, + contentDescription = "Play", colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground), modifier = Modifier.align(Alignment.Center), ) @@ -63,15 +63,15 @@ fun TimelineItemVideoView( @Preview @Composable -internal fun TimelineItemVideoViewLightPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) = +internal fun TimelineItemVideoViewLightPreview(@PreviewParameter(TimelineItemVideoContentProvider::class) content: TimelineItemVideoContent) = ElementPreviewLight { ContentToPreview(content) } @Preview @Composable -internal fun TimelineItemVideoViewDarkPreview(@PreviewParameter(TimelineItemImageContentProvider::class) content: TimelineItemImageContent) = +internal fun TimelineItemVideoViewDarkPreview(@PreviewParameter(TimelineItemVideoContentProvider::class) content: TimelineItemVideoContent) = ElementPreviewDark { ContentToPreview(content) } @Composable -private fun ContentToPreview(content: TimelineItemImageContent) { - TimelineItemImageView(content) +private fun ContentToPreview(content: TimelineItemVideoContent) { + TimelineItemVideoView(content) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt index eb4a15d378..0f50858e06 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt @@ -33,9 +33,9 @@ fun aTimelineItemVideoContent() = TimelineItemVideoContent( thumbnailSource = MediaSource(url = ""), blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr", aspectRatio = 0.5f, - duration = 0, + duration = 100, videoSource = MediaSource(""), - height = null, - width = null, + height = 300, + width = 150, mimeType = null ) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png index efe83873c0..a7e6d29b5f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7787897d8dda469f17da7151260f26b6dcc5500f93ce44fb317af42ae8d457c -size 93739 +oid sha256:cbfbfa08085539bbfa4f20bc5d149bf0db0036169a32747c96f956d0d0f4b42d +size 93741 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png index 6dae242f3c..e278779a22 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6691512398d2c7e319d15397120054f1c2aae452ffed0be47df72ecb906ac4fa -size 93675 +oid sha256:9e0ba60e784a0dc582826a3f5679a6015531dc9aca22617ec1e0dede38b97f30 +size 93678 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cd4deb3d0b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:372f239e351215dadf0d5c451b8105e93bc86e338f4e564aa4726d482028ae9c +size 396027 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5183011e59 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:087ef6e2e489e63e1a30793b183915984a32265676fd5a95f02d7ca02821c84b +size 132174 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..62f356b12c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.attachments.preview_null_DefaultGroup_AttachmentsPreviewViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5e03b31f020201e8a8bd7ad5962022337441c4e83d15e03e95f83c9fa10eaaf +size 98743 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..695a886bc1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4579f180e57cb1a277a372b66638b98e16eccd256140ed5c3ffd97d4ff79141 +size 4655 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..695a886bc1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4579f180e57cb1a277a372b66638b98e16eccd256140ed5c3ffd97d4ff79141 +size 4655 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ce5c788964 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb123ac90485b02cbf4794094549d5edd05f7f4f433e2f57fc51039035d509a9 +size 12309 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4afab509fc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479 +size 393620 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.textcomposer_null_DefaultGroup_MessageComposerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.messagecomposer_null_DefaultGroup_MessageComposerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.textcomposer_null_DefaultGroup_MessageComposerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.messagecomposer_null_DefaultGroup_MessageComposerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.textcomposer_null_DefaultGroup_MessageComposerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.messagecomposer_null_DefaultGroup_MessageComposerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.textcomposer_null_DefaultGroup_MessageComposerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.messagecomposer_null_DefaultGroup_MessageComposerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b796a0c3e2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:980cd5708f08c41cdb1884d523d364ba0ac5dbe28c5172f5b74f08409edb195f +size 8131 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e53c707e7d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe65c1d4a11e2c1e370b1c68819ac960101a6436b48ab87b8c7a4f8a36d282b8 +size 7978 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index f3c044b7c4..b4c0f98f8f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:894636cd0aaeeb06b2117b96cbb0d4de5edf94eeb468134efbdebf86b623e082 -size 270816 +oid sha256:089bd39f60e60ed9398d29cccdc759bb63e13ec77d85d945617db946c211281f +size 277105 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 964891e6d2..f3f217350d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44313efc4d87857b9eb0bb05c8d24330b17353fd95344bcce06c1ad1bba8df81 -size 492167 +oid sha256:fa1b524383835bce48df4fe45e372beecc88381c55195fecfda597da9252d92d +size 449354 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index b59ec5f401..006bc929cb 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2dd9d63fe0a80bd1a5e1188b2ba4d05c99cba56fc82864fe7e62f394714d627 -size 805682 +oid sha256:dc13b4f4dfb8874c0d02043a0ea4d02c904583df7ac185c5df4664c0dc8460a8 +size 405110 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index ce023c753c..b4c0f98f8f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:494e968f45a1ae8ec2e2f648d71b6024ec049f3a6f8ed93108d3084d0fe13185 -size 271700 +oid sha256:089bd39f60e60ed9398d29cccdc759bb63e13ec77d85d945617db946c211281f +size 277105 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 964891e6d2..f3f217350d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44313efc4d87857b9eb0bb05c8d24330b17353fd95344bcce06c1ad1bba8df81 -size 492167 +oid sha256:fa1b524383835bce48df4fe45e372beecc88381c55195fecfda597da9252d92d +size 449354 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 76901beb9a..006bc929cb 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d680b2a314a2663ca14cb658ac35e2060da9f52823c0ec4844e3a7a4756e0dd4 -size 806726 +oid sha256:dc13b4f4dfb8874c0d02043a0ea4d02c904583df7ac185c5df4664c0dc8460a8 +size 405110 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7d2e7439af --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94050129b61f7da2dea8013463a5ebcdba22ee6034f74ca3609327c05edc42f7 +size 276900 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d36ef57f17 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5740e96abf53d01094b6db6ff36173de56e0796b4dd42565eac9a1fa9db4950d +size 449497 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2465f23fff --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c03d024d6012838414ce7c3eaad7aedaffaeffd7dbd501c86002c209ef410f6a +size 405830 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..10576c34ad --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a30f13f1783170cd5e3527d9fe9f0603fb50b0ef9ddcd3489b89d9da4dd5bb6f +size 276854 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6aed9c864e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d12e8522b7fdfd47726ee0b0f6673aebbb76bf83ff27bfe5c6459ebf3da263d4 +size 449485 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6ccbdaf5e7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdea573f5d2d521f6e22d599aded57fc896fc015dfda275c05695f9ce8c0f6be +size 405887 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 17e1d2852a..7d93e5c715 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:523d6607d70a331d2ec32aeabf033d59c4dc60e0e7373982893df0245ae924ce -size 3332 +oid sha256:a3443f60b8ceec7eccfee92c1bca8cad151e25cff8e7845459a5f4c3d50e5e08 +size 3330 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png index abfa1dae68..7b025a402e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:266490a1bdce29ac65d875543deda072d7f2a9aa2e216efb0d54b93c2a11aa5f -size 4001 +oid sha256:d0b0cdb7e39d5ca5ce874477da4397ebed00ecf9f5d8980f9f3b03ab72259d9f +size 4000 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 17e1d2852a..7d93e5c715 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:523d6607d70a331d2ec32aeabf033d59c4dc60e0e7373982893df0245ae924ce -size 3332 +oid sha256:a3443f60b8ceec7eccfee92c1bca8cad151e25cff8e7845459a5f4c3d50e5e08 +size 3330 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png index abfa1dae68..7b025a402e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:266490a1bdce29ac65d875543deda072d7f2a9aa2e216efb0d54b93c2a11aa5f -size 4001 +oid sha256:d0b0cdb7e39d5ca5ce874477da4397ebed00ecf9f5d8980f9f3b03ab72259d9f +size 4000 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png index 66a540e8a7..eb0f1bcfde 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e94515b43bc4b8427edc60456ec657ec38b33539a034a2c45c92624a6b75d4e8 -size 3138 +oid sha256:d35eb1aac0d413c216dc77f45f655be97ae9d36c3cade57ee8f70e25c44fafd1 +size 3142 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png index 6a087cd16c..69b558a480 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4dc928a04bfc6331673be2fa0bf7f12e384dfd8ff91b07dc57b220c8dbbdd71 -size 3829 +oid sha256:782a439312e4b616a6dd3020d8c99d04042208b2ebc416fab73c95344adeba67 +size 3834 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png index 66a540e8a7..eb0f1bcfde 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e94515b43bc4b8427edc60456ec657ec38b33539a034a2c45c92624a6b75d4e8 -size 3138 +oid sha256:d35eb1aac0d413c216dc77f45f655be97ae9d36c3cade57ee8f70e25c44fafd1 +size 3142 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png index 6a087cd16c..69b558a480 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4dc928a04bfc6331673be2fa0bf7f12e384dfd8ff91b07dc57b220c8dbbdd71 -size 3829 +oid sha256:782a439312e4b616a6dd3020d8c99d04042208b2ebc416fab73c95344adeba67 +size 3834 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png index 89034f66b9..d6eab0d361 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25d0334932490997d432115c550c42e74e7226399ef82031430bf86ba33972bd -size 6066 +oid sha256:6cab21ee595c96700e4577554f9c98fdc56a59bf5093aa9d81a54d10381ba122 +size 6065 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png index c407012dfd..70722aa53f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2d15387248308e5234c84b9b2b282e736f05f0495c80807bacfb0acff31eb2b -size 5890 +oid sha256:b9235621126fe90a51d557df616878d53120dada814830fba006b54f1556e69e +size 5895 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 3fc91b65e4..b11864d276 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5315b975f8b9def0b0d7575160cb34300511f1b83845ee2d1762188481cf6fe9 -size 30927 +oid sha256:562e83b7771ff2209c7882180af55a8f3123c12c020b294858b1b46bff8a8a95 +size 30928 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 7a61ecf737..a55c0f8cdc 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:099401d03621aca301d3c4590d90ba4e4642d400d7603a8e15140a1bfef0c0ec +oid sha256:79b16cbbbf36e6de4ea6270476371c7097424b26d4066f7156738c084fe061de size 42758 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index e361e4a2d2..f1bd1303ce 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca5dbc3aa00d6116de38fc0217b20ab0db4096b1f76bcc18b43ab9fd343f8110 -size 32722 +oid sha256:7534d8379a1ab220833b8c877f9b3021a665cdb97c71ba383695276d83757702 +size 32721 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png index 42f22c9a26..2fa40047c6 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6211ec07d10d48e4713206e4c1fe24eae890fe2310d5bcc1f4ca54cc57f84ec9 -size 44564 +oid sha256:a3123fce3d4e4ed0480d2757e4daa70ecd14ddad1e8471523638561cccbab8dc +size 44568 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png index b974aec366..d0b80fcd37 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ac7985e70782a84fc351cb15536bfd700ca8d6d48d5da0ee9dcaf5c78226479 -size 28909 +oid sha256:32d8032b1b2b3ae2b2795dea60b6395104df24790450f9452bbe82e81d9e62e8 +size 28911 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png index cf23661de1..c14452b104 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c99138940ed1a8beac7da3c673600016a03b87195e07f205d5d389ef05be0fb0 -size 45978 +oid sha256:abff6db46d39ec39f3c0d5366f91252b58cbf62e6850bc86a6e81e52c80ba096 +size 45979 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 20f62bf8e6..3123c5dd15 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f24112ad89e5ef93917d45e3232043168d5092ce6d62a7d414773dd0afda963 -size 30326 +oid sha256:34469e4b86a441fb6f7cfbdce4b622109c093a234ae5d35d895dce35ef10c20c +size 30327 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 534402d370..6d5c269d4c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c696c464515873a94d28f8909dc47255717fc3b012ce0d80000769714e3cb9c -size 42427 +oid sha256:11c677ab3124a94f531f1db14eb2b2f19a493470620f7375cf7c1d14056cdbc9 +size 42429 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 88014ea46b..3b8db29b08 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fe506e4e7adbfe19fd48f0a40c19343471e0bb06e12fac8d5e6d54595e7193a -size 32076 +oid sha256:9a35878a3d306b1a6d1768669904ee1f9e0a71abd4ab3c04ff6564027732614f +size 32077 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png index e70fa9d639..816518f7aa 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9c9374c303c9a71b0526fccfd18903d145c1b3b0059de3ccd41cf80f1541a80 -size 44427 +oid sha256:c1b3b0b538dafa85849c6e255bcff6b1a82a52d49be1fe4ded9b7f7c2ec06bff +size 44431 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png index 4ebc61d047..25f8d272a1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f18d8b33eace857f78b92eed035621125bbfca1f0d43e5e47fd1c3965324de0 -size 28319 +oid sha256:ef18369400f738e94c5a812dc0ec4c8473659eafb11d7361385647ba6a640d15 +size 28320 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png index 336334e955..49caf143da 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be0722a804c2fc264f931f8784635bceab7ddcf08e378bed8ea510bd76813fbf -size 45642 +oid sha256:633210082fc83be3628250fa35768969aad136035924667bd2f3c6c8307fd87c +size 45641 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 1ba319f8ee..580da9ceb6 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b77478183675fd3bb9a7ef34ef808e3ad75b504a0236b4dcb0d96a7faafc29cc -size 39629 +oid sha256:01f54909964a4ed07d8850ab2bffad8b99ed641d731c1808b04f164ff3e0cbba +size 39632 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 27b4e07cb9..ff59a95d62 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2645c93a097d35725a7afb83c97dd0ea6b5623990cbae2372aaf66c9011e353c -size 41506 +oid sha256:4ea39fdf1cb61657bc1eafb7d353ee15517c26c0e6bb2804b7c8350fa651299e +size 41508 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index dee6d7dd1a..e6d1c48263 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:394ffe0b730e7d4010ec0d7f96b5487b2775e70aa7e42b6bf8b2535ad5acc244 -size 37524 +oid sha256:aeec498cb7f1c2210085b5ae7456d0b211ed35abbebffe959e24921c278566da +size 39568 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index 4aa436dc8c..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b87be006b0125029391e74a09bc2a07a906c33d9ea21ca17f884f2d8862af32a -size 36302 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index c3e23147ca..34de0f83a1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93f8dd057f245d454a6ebf81607c879b6ae4c20287a64d001bee36764e0802a5 -size 38657 +oid sha256:cd32f8da6c0547f0cea178d50b281d02d651bdaa49d80143a7a01b9f4cfb429c +size 38654 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index c3d01356c6..c4d2e0d1b4 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a4ce62d7f8f27128fd59cd3e7eea068a002d7e2c0a054fef1f555d06cf91ebe +oid sha256:cfb73e70d2b438521a11560587080af0adf0e43553806bc35cdb64b3c7944fde size 40708 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 4660650dfb..121379617e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bee8752a24938844cae76aadae9148a19001302a9e4909cf69a192a98d583ba4 -size 36230 +oid sha256:9e8b8baa1425214daebf83f60cceeba8ac63be28da9d9949ea36e1f2e044e2ea +size 37533 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index 6e04daf76b..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8f89eebcabe514594c53e6e436ae63685e6b693628f2acf796c6306298aff1a8 -size 35199 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png index b1f708d57c..9749477ebd 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b53d55b5085673ac3a0a7663f7ff68e7d510fe352f3931b49c7e75e4c3767b93 -size 60007 +oid sha256:c820bd324df729db08710d1a6c17ca34a451a3bb94da2206fc57d6b4efe91e2f +size 60019 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png index d020b1e521..f24edcc4a1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a761318f3dfbc2ce6e777cfabc19eeb2f89100ae7631660f4b4d7550cd947c84 -size 57580 +oid sha256:da2f9f87d89382b4b18d39a477a8e86f98067e2326adae4def8374b5bf09f316 +size 57588 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 01ec8ec0fc..07451a0330 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:147786f2cfcf7674147253cca5e41b4af96587a27216076a1c0802c81b0b1b46 +oid sha256:628f1f00dca9d15faabd8288a3c54b1b8581a380cf14edf364f0dee9ecc187d5 size 180117 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index a49d11835b..8146c241cb 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.rageshake.impl.bugreport_null_DefaultGroup_BugReportViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:789a979f7207531b4b9ba488ae2de52e6046809e788baf6690e1383a933cf624 -size 178916 +oid sha256:827bccb0fdd3106fb24a95d665b4da2cfc15de48e8f508ae809c9f75d6d1bbf0 +size 178915 From c741b35035e2157702d977d6c57304ffeb3d31f5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 May 2023 16:21:53 +0200 Subject: [PATCH 34/48] Media: polish TimelineVideo item --- .../components/blurhash/BlurHashAsyncImage.kt | 1 + .../components/event/TimelineItemVideoView.kt | 29 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt index f3f2addfa7..9237e87e9d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/blurhash/BlurHashAsyncImage.kt @@ -50,6 +50,7 @@ fun BlurHashAsyncImage( contentAlignment = Alignment.Center, ) { AsyncImage( + modifier = Modifier.fillMaxSize(), model = model, contentScale = contentScale, contentDescription = contentDescription, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index 56c55e4ef2..84f27720d1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -17,17 +17,23 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box 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.filled.PlayCircle -import androidx.compose.material3.MaterialTheme +import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.timeline.components.blurhash.BlurHashAsyncImage import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContentProvider @@ -52,12 +58,19 @@ fun TimelineItemVideoView( modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Fit, ) - Image( - Icons.Default.PlayCircle, - contentDescription = "Play", - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground), - modifier = Modifier.align(Alignment.Center), - ) + Box( + modifier = Modifier + .size(50.dp) + .clip(CircleShape) + .background(color = Color.Black.copy(alpha = 0.5f)), + contentAlignment = Alignment.Center, + ) { + Image( + Icons.Default.PlayArrow, + contentDescription = "Play", + colorFilter = ColorFilter.tint(Color.White), + ) + } } } From 5e5737aa61b03e7bf4ee9a4a315e82e41fa71c8e Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 May 2023 17:47:53 +0200 Subject: [PATCH 35/48] Media: add some previews in the TimelineView --- .../messages/impl/timeline/TimelineStateProvider.kt | 4 ++-- .../features/messages/impl/timeline/TimelineView.kt | 4 +--- .../model/event/TimelineItemEventContentProvider.kt | 5 ++++- .../model/event/TimelineItemFileContentProvider.kt | 10 ++++++---- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index f6eaa55267..189c1769ee 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -30,8 +30,8 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlin.random.Random -fun aTimelineState() = TimelineState( - timelineItems = persistentListOf(), +fun aTimelineState(timelineItems: ImmutableList = persistentListOf()) = TimelineState( + timelineItems = timelineItems, paginationState = MatrixTimeline.PaginationState(isBackPaginating = false, canBackPaginate = true), highlightedEventId = null, eventSink = {} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 11a3985c9d..1114ddbf4e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -332,8 +332,6 @@ fun TimelineViewDarkPreview( private fun ContentToPreview(content: TimelineItemEventContent) { val timelineItems = aTimelineItemList(content) TimelineView( - state = aTimelineState().copy( - timelineItems = timelineItems, - ) + state = aTimelineState(timelineItems) ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt index 7e4be1bbd1..30b5f29ea0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt @@ -24,7 +24,10 @@ class TimelineItemEventContentProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aTimelineItemFileContent(), + aTimelineItemFileContent("A file.pdf"), + aTimelineItemFileContent("A bigger name file.pdf"), + aTimelineItemFileContent("An even bigger file name which doesn't fit.pdf"), ) } -fun aTimelineItemFileContent() = TimelineItemFileContent( - body = "A nice file with a nice name.pdf", +fun aTimelineItemFileContent(fileName: String) = TimelineItemFileContent( + body = fileName, thumbnailSource = MediaSource(url = ""), fileSource = MediaSource(url = ""), - mimeType = MimeTypes.Apk, + mimeType = MimeTypes.OctetStream, size = 100 ) From e0106fe90740f79c687d1d12f448a8ba0fc66463 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 25 May 2023 22:54:09 +0200 Subject: [PATCH 36/48] Media: some more improvements over MediaViewer --- .../messages/impl/MessagesFlowNode.kt | 29 +++-- .../messages/impl/media/local/LocalMedia.kt | 2 + .../impl/media/local/LocalMediaView.kt | 50 +++++-- .../impl/media/viewer/MediaViewerEvents.kt | 1 + .../impl/media/viewer/MediaViewerNode.kt | 1 + .../impl/media/viewer/MediaViewerPresenter.kt | 3 + .../impl/media/viewer/MediaViewerState.kt | 5 +- .../media/viewer/MediaViewerStateProvider.kt | 14 +- .../impl/media/viewer/MediaViewerView.kt | 122 ++++++++++++++---- .../components/event/TimelineItemVideoView.kt | 13 +- .../TimelineItemContentMessageFactory.kt | 8 +- .../event/TimelineItemImageContentProvider.kt | 6 +- .../model/event/TimelineItemVideoContent.kt | 2 +- .../event/TimelineItemVideoContentProvider.kt | 2 +- .../modifiers/RoundedBackground.kt | 40 ++++++ .../android/libraries/matrix/test/TestData.kt | 2 - 16 files changed, 226 insertions(+), 74 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/RoundedBackground.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index c8e35ccbe8..1e10a553f1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -26,7 +26,6 @@ import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.push -import com.bumble.appyx.navmodel.backstack.transitionhandler.rememberBackstackFader import dagger.assisted.Assisted import dagger.assisted.AssistedInject 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.di.RoomScope import io.element.android.libraries.matrix.api.media.MediaSource -import kotlinx.android.parcel.Parcelize import kotlinx.collections.immutable.ImmutableList +import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) class MessagesFlowNode @AssistedInject constructor( @@ -65,7 +64,8 @@ class MessagesFlowNode @AssistedInject constructor( data class MediaViewer( val title: String, val mediaSource: MediaSource, - val mimeType: String? + val thumbnailSource: MediaSource?, + val mimeType: String?, ) : NavTarget @Parcelize @@ -93,7 +93,12 @@ class MessagesFlowNode @AssistedInject constructor( createNode(buildContext, listOf(callback)) } 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(buildContext, listOf(inputs)) } is NavTarget.AttachmentPreview -> { @@ -106,13 +111,22 @@ class MessagesFlowNode @AssistedInject constructor( private fun processEventClicked(event: TimelineItem.Event) { when (event.content) { is TimelineItemImageContent -> { - val mediaSource = event.content.mediaSource - val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource, event.content.mimeType) + val navTarget = NavTarget.MediaViewer( + title = event.content.body, + mediaSource = event.content.mediaSource, + thumbnailSource = event.content.mediaSource, + mimeType = event.content.mimeType + ) backstack.push(navTarget) } is TimelineItemVideoContent -> { 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) } else -> Unit @@ -124,7 +138,6 @@ class MessagesFlowNode @AssistedInject constructor( Children( navModel = backstack, modifier = modifier, - transitionHandler = rememberBackstackFader() ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt index 41ff1025ff..8305c8eee7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt @@ -18,10 +18,12 @@ package io.element.android.features.messages.impl.media.local import android.net.Uri import android.os.Parcelable +import androidx.compose.runtime.Immutable import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize +@Immutable data class LocalMedia( val uri: Uri, val mimeType: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt index 1f3a67a8ec..8c94822947 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.media3.common.MediaItem import androidx.media3.common.MimeTypes +import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.AspectRatioFrameLayout @@ -47,16 +48,20 @@ import me.saket.telephoto.zoomable.rememberZoomableState @SuppressLint("UnsafeOptInUsageError") @Composable fun LocalMediaView( - localMedia: LocalMedia, - modifier: Modifier = Modifier + localMedia: LocalMedia?, + modifier: Modifier = Modifier, + mimeType: String? = localMedia?.mimeType, + onReady: () -> Unit = {}, ) { when { - MimeTypes.isImage(localMedia.mimeType) -> MediaImageView( + MimeTypes.isImage(mimeType) -> MediaImageView( localMedia = localMedia, + onReady = onReady, modifier = modifier ) - MimeTypes.isVideo(localMedia.mimeType) -> MediaVideoView( + MimeTypes.isVideo(mimeType) -> MediaVideoView( localMedia = localMedia, + onReady = onReady, modifier = modifier ) else -> Unit @@ -65,7 +70,8 @@ fun LocalMediaView( @Composable private fun MediaImageView( - localMedia: LocalMedia, + localMedia: LocalMedia?, + onReady: () -> Unit, modifier: Modifier = Modifier, ) { if (LocalInspectionMode.current) { @@ -78,10 +84,16 @@ private fun MediaImageView( val zoomableState = rememberZoomableState( zoomSpec = ZoomSpec(maxZoomFactor = 3f) ) + val zoomableImageState = rememberZoomableImageState(zoomableState) + LaunchedEffect(zoomableImageState.isImageDisplayed) { + if (zoomableImageState.isImageDisplayed) { + onReady() + } + } ZoomableAsyncImage( modifier = modifier.fillMaxSize(), - state = rememberZoomableImageState(zoomableState), - model = localMedia.model, + state = zoomableImageState, + model = localMedia?.model, contentDescription = "Image", contentScale = ContentScale.Fit, ) @@ -91,22 +103,32 @@ private fun MediaImageView( @UnstableApi @Composable fun MediaVideoView( - localMedia: LocalMedia, + localMedia: LocalMedia?, + onReady: () -> Unit, modifier: Modifier = Modifier, ) { val context = LocalContext.current + val playerListener = object : Player.Listener { + override fun onRenderedFirstFrame() { + onReady() + } + } val exoPlayer = remember { - ExoPlayer.Builder(context).build() + ExoPlayer.Builder(context) + .build() .apply { - this.playWhenReady = true + addListener(playerListener) this.prepare() } } - LaunchedEffect(localMedia.uri) { - val mediaItem = MediaItem.fromUri(localMedia.uri) - exoPlayer.setMediaItem(mediaItem) + if (localMedia?.uri != null) { + LaunchedEffect(localMedia.uri) { + val mediaItem = MediaItem.fromUri(localMedia.uri) + exoPlayer.setMediaItem(mediaItem) + } + } else { + exoPlayer.setMediaItems(emptyList()) } - AndroidView( factory = { PlayerView(context).apply { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt index 4b60c05b3d..b0bbad5ec2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt @@ -18,4 +18,5 @@ package io.element.android.features.messages.impl.media.viewer sealed interface MediaViewerEvents { object RetryLoading : MediaViewerEvents + object ClearLoadingError : MediaViewerEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt index 7321eff416..247a86263f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt @@ -40,6 +40,7 @@ class MediaViewerNode @AssistedInject constructor( data class Inputs( val name: String, val mediaSource: MediaSource, + val thumbnailSource: MediaSource?, val mimeType: String? ) : NodeInputs diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt index 542436ed9f..01b562f62c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt @@ -67,11 +67,14 @@ class MediaViewerPresenter @AssistedInject constructor( fun handleEvents(mediaViewerEvents: MediaViewerEvents) { when (mediaViewerEvents) { MediaViewerEvents.RetryLoading -> loadMediaTrigger++ + MediaViewerEvents.ClearLoadingError -> localMedia.value = Async.Uninitialized } } return MediaViewerState( name = inputs.name, + mimeType = inputs.mimeType, + thumbnailSource = inputs.thumbnailSource, downloadedMedia = localMedia.value, eventSink = ::handleEvents ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt index b5279af6c8..c42263dd3e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt @@ -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.libraries.architecture.Async +import io.element.android.libraries.matrix.api.media.MediaSource data class MediaViewerState( val name: String, + val mimeType: String?, + val thumbnailSource: MediaSource?, val downloadedMedia: Async, - val eventSink: (MediaViewerEvents) -> Unit + val eventSink: (MediaViewerEvents) -> Unit, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt index 45ac530b8e..6c54eb3b05 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt @@ -31,7 +31,14 @@ open class MediaViewerStateProvider : PreviewParameterProvider aMediaViewerState( Async.Success( 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 fun aMediaViewerState(downloadedMedia: Async = Async.Uninitialized) = MediaViewerState( name = "A media", + mimeType = MimeTypes.IMAGE_JPEG, + thumbnailSource = null, downloadedMedia = downloadedMedia, - eventSink = {} -) +) {} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt index b5ef22b596..b2957cac18 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -14,33 +14,36 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterial3Api::class) package io.element.android.features.messages.impl.media.viewer 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.fillMaxWidth 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.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.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview 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.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.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.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 @Composable @@ -53,6 +56,32 @@ fun MediaViewerView( 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) { Box( modifier = Modifier @@ -60,10 +89,55 @@ fun MediaViewerView( .padding(it), contentAlignment = Alignment.Center ) { - when (state.downloadedMedia) { - is Async.Success -> LocalMediaView(state.downloadedMedia.state) - is Async.Failure -> ErrorView(stringResource(id = StringR.string.error_unknown), ::onRetry) - else -> CircularProgressIndicator() + if (state.downloadedMedia is Async.Failure) { + ErrorView( + errorMessage = stringResource(id = StringR.string.error_unknown), + 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( errorMessage: String, onRetry: () -> Unit, + onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text(text = errorMessage) - Spacer(modifier = Modifier.size(16.dp)) - Button( - onClick = onRetry - ) { - Text(text = stringResource(id = StringR.string.action_retry)) - } - - } + RetryDialog( + modifier = modifier, + content = errorMessage, + onRetry = onRetry, + onDismiss = onDismiss + ) } @Preview diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index 84f27720d1..883f7b30f3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -17,26 +17,22 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box 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.filled.PlayArrow import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.timeline.components.blurhash.BlurHashAsyncImage 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.libraries.designsystem.modifiers.roundedBackground import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.matrix.ui.media.MediaRequestData @@ -54,15 +50,12 @@ fun TimelineItemVideoView( ) { BlurHashAsyncImage( model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.Content), - blurHash = content.blurhash, + blurHash = content.blurHash, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Fit, ) Box( - modifier = Modifier - .size(50.dp) - .clip(CircleShape) - .background(color = Color.Black.copy(alpha = 0.5f)), + modifier = Modifier.roundedBackground(), contentAlignment = Alignment.Center, ) { Image( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index 83163741c5..db68201f9c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -46,11 +46,11 @@ class TimelineItemContentMessageFactory @Inject constructor() { val aspectRatio = aspectRatioOf(messageType.info?.width, messageType.info?.height) TimelineItemImageContent( body = messageType.body, - height = messageType.info?.height?.toInt(), - width = messageType.info?.width?.toInt(), - mimeType = messageType.info?.mimetype, mediaSource = messageType.source, + mimeType = messageType.info?.mimetype, blurhash = messageType.info?.blurhash, + width = messageType.info?.width?.toInt(), + height = messageType.info?.height?.toInt(), aspectRatio = aspectRatio ) } @@ -64,7 +64,7 @@ class TimelineItemContentMessageFactory @Inject constructor() { width = messageType.info?.width?.toInt(), height = messageType.info?.height?.toInt(), duration = messageType.info?.duration ?: 0L, - blurhash = messageType.info?.blurhash, + blurHash = messageType.info?.blurhash, aspectRatio = aspectRatio ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt index 6324789d8f..97bbf6ed41 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt @@ -32,9 +32,9 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider Date: Thu, 25 May 2023 23:02:55 +0200 Subject: [PATCH 37/48] Media: generate screenshots again --- ...up_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...up_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...up_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...up_MediaViewerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 3 +++ ...melineItemFileViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...melineItemFileViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...melineItemFileViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...elineItemFileViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...elineItemFileViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...elineItemFileViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...elineItemVideoViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...elineItemVideoViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...elineItemVideoViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...lineItemVideoViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...lineItemVideoViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...lineItemVideoViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 3 +++ ...Group_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png | 3 +++ ...Group_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png | 3 +++ ...Group_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png | 3 +++ ...roup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png | 3 +++ ...roup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png | 3 +++ ...roup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png | 3 +++ ...roup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png | 3 +++ 32 files changed, 77 insertions(+), 38 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 695a886bc1..4afab509fc 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4579f180e57cb1a277a372b66638b98e16eccd256140ed5c3ffd97d4ff79141 -size 4655 +oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479 +size 393620 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 695a886bc1..4afab509fc 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4579f180e57cb1a277a372b66638b98e16eccd256140ed5c3ffd97d4ff79141 -size 4655 +oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479 +size 393620 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index ce5c788964..4afab509fc 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb123ac90485b02cbf4794094549d5edd05f7f4f433e2f57fc51039035d509a9 -size 12309 +oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479 +size 393620 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4afab509fc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd0b34e2f0b0c8e7267b4029fb7755c707ebf0e018d085d7374ebd459fbe7479 +size 393620 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index b796a0c3e2..a37123fc45 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:980cd5708f08c41cdb1884d523d364ba0ac5dbe28c5172f5b74f08409edb195f -size 8131 +oid sha256:08637476f7fa306582ce8c1c29cdd2a8b6ffecf50fee7dc5b18ad04e55cf3da1 +size 4015 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d63055a7dc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8b596c8c1787e84581a64cee001a003d7aeef3ec1d73be1491e44b910d9bc44 +size 7059 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ca21c3cf9c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:176dd8c0092e9a4762571ca73d307eb590048f309e0d7318c726741d8deb2142 +size 17367 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index e53c707e7d..a987044f72 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe65c1d4a11e2c1e370b1c68819ac960101a6436b48ab87b8c7a4f8a36d282b8 -size 7978 +oid sha256:27028c8e52e0a3285ca42d873f97bc95ea7a7a455ca2cc41811397e715cde314 +size 3969 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..05f118524c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e70df87f6361ccc10c932c63cbcbdeb1e1c94463bfb4419406d9593a4f8a1ec7 +size 6992 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ec5ba12fc6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemFileViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47f6e7050eafade13b10b2576a4ea03aaa22728fc6c50ab8cfc5f5c4d2e7b983 +size 14811 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 7d2e7439af..c9496bda0f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94050129b61f7da2dea8013463a5ebcdba22ee6034f74ca3609327c05edc42f7 -size 276900 +oid sha256:ff823c52d1a8b890bbddb95fea74d06983c005cd1ac770a16882c316287ef725 +size 275303 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index d36ef57f17..5c8d11a742 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5740e96abf53d01094b6db6ff36173de56e0796b4dd42565eac9a1fa9db4950d -size 449497 +oid sha256:e7d31a8eba23df0f2449a1721abb49e9e723b9015fefb5dfb5457357e10ba70a +size 448675 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 2465f23fff..932aaccc4d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c03d024d6012838414ce7c3eaad7aedaffaeffd7dbd501c86002c209ef410f6a -size 405830 +oid sha256:88ea27cbaf0fc3952e6b02073924bd81e6d4e25efbdee1bf7cbb4b9950f295ef +size 405641 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 10576c34ad..c9496bda0f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a30f13f1783170cd5e3527d9fe9f0603fb50b0ef9ddcd3489b89d9da4dd5bb6f -size 276854 +oid sha256:ff823c52d1a8b890bbddb95fea74d06983c005cd1ac770a16882c316287ef725 +size 275303 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 6aed9c864e..5c8d11a742 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d12e8522b7fdfd47726ee0b0f6673aebbb76bf83ff27bfe5c6459ebf3da263d4 -size 449485 +oid sha256:e7d31a8eba23df0f2449a1721abb49e9e723b9015fefb5dfb5457357e10ba70a +size 448675 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 6ccbdaf5e7..932aaccc4d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bdea573f5d2d521f6e22d599aded57fc896fc015dfda275c05695f9ce8c0f6be -size 405887 +oid sha256:88ea27cbaf0fc3952e6b02073924bd81e6d4e25efbdee1bf7cbb4b9950f295ef +size 405641 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index f1bd1303ce..519b619f79 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7534d8379a1ab220833b8c877f9b3021a665cdb97c71ba383695276d83757702 -size 32721 +oid sha256:32ef3493e52faae7fa8c3dc1c2757212f3649c3dad151062d9fa6e5f49a541e7 +size 192498 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png index 2fa40047c6..bdbd67a7ad 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3123fce3d4e4ed0480d2757e4daa70ecd14ddad1e8471523638561cccbab8dc -size 44568 +oid sha256:6ea74556d6594ce3805b59e5222480ca5a779704f6f358f5e227549d868bc3e3 +size 192720 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png index d0b80fcd37..1cf35816aa 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32d8032b1b2b3ae2b2795dea60b6395104df24790450f9452bbe82e81d9e62e8 -size 28911 +oid sha256:898b99d1603073bd8a047c5ab4c40ae221b88ed3167028d613fbff8e0907a84b +size 40601 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png index c14452b104..5db287217d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abff6db46d39ec39f3c0d5366f91252b58cbf62e6850bc86a6e81e52c80ba096 -size 45979 +oid sha256:ae648857da2faa4af355759b149cdb251f8f8f9563d0dd2d7543c5431f756454 +size 62756 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f1bd1303ce --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7534d8379a1ab220833b8c877f9b3021a665cdb97c71ba383695276d83757702 +size 32721 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2fa40047c6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3123fce3d4e4ed0480d2757e4daa70ecd14ddad1e8471523638561cccbab8dc +size 44568 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d0b80fcd37 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32d8032b1b2b3ae2b2795dea60b6395104df24790450f9452bbe82e81d9e62e8 +size 28911 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c14452b104 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abff6db46d39ec39f3c0d5366f91252b58cbf62e6850bc86a6e81e52c80ba096 +size 45979 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 3b8db29b08..5a8bba8f27 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a35878a3d306b1a6d1768669904ee1f9e0a71abd4ab3c04ff6564027732614f -size 32077 +oid sha256:c88f3a942de1018f4bcaefe0e86147e7c7c67d67e31aa259ce76c186cac09632 +size 194017 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png index 816518f7aa..21a9a3848e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1b3b0b538dafa85849c6e255bcff6b1a82a52d49be1fe4ded9b7f7c2ec06bff -size 44431 +oid sha256:01c7fa298c32e5459609cc0c8f3bd770986320b2595fcbdcc014e58d9fd15e51 +size 194261 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png index 25f8d272a1..66eb095367 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef18369400f738e94c5a812dc0ec4c8473659eafb11d7361385647ba6a640d15 -size 28320 +oid sha256:878976e75768bf17e753b343ef887f344d523d819c9b8adb804fc7c380827d8c +size 40395 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png index 49caf143da..b37616000b 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:633210082fc83be3628250fa35768969aad136035924667bd2f3c6c8307fd87c -size 45641 +oid sha256:12519c3717f0281e2d303b1c9c63725ebae55a1a9d503d8e4a63d95375d6dcc5 +size 63441 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3b8db29b08 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a35878a3d306b1a6d1768669904ee1f9e0a71abd4ab3c04ff6564027732614f +size 32077 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..816518f7aa --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1b3b0b538dafa85849c6e255bcff6b1a82a52d49be1fe4ded9b7f7c2ec06bff +size 44431 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..25f8d272a1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef18369400f738e94c5a812dc0ec4c8473659eafb11d7361385647ba6a640d15 +size 28320 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..49caf143da --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:633210082fc83be3628250fa35768969aad136035924667bd2f3c6c8307fd87c +size 45641 From 2ec2c9b16f51ca2102c0992f7bf58894b9b90ec5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 26 May 2023 11:29:49 +0200 Subject: [PATCH 38/48] Media: continue improving VideoView --- .../impl/media/local/LocalMediaView.kt | 12 +++-- .../media/local/exoplayer/ExoPlayerWrapper.kt | 49 +++++++++++++++++++ .../impl/media/viewer/MediaViewerView.kt | 48 ++++++++++-------- 3 files changed, 85 insertions(+), 24 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/exoplayer/ExoPlayerWrapper.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt index 8c94822947..3040f0bfbd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt @@ -35,9 +35,9 @@ import androidx.media3.common.MediaItem import androidx.media3.common.MimeTypes import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView +import io.element.android.features.messages.impl.media.local.exoplayer.ExoPlayerWrapper import io.element.android.libraries.designsystem.R import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import me.saket.telephoto.zoomable.ZoomSpec @@ -114,8 +114,7 @@ fun MediaVideoView( } } val exoPlayer = remember { - ExoPlayer.Builder(context) - .build() + ExoPlayerWrapper.create(context) .apply { addListener(playerListener) this.prepare() @@ -133,6 +132,8 @@ fun MediaVideoView( factory = { PlayerView(context).apply { player = exoPlayer + setShowPreviousButton(false) + setShowNextButton(false) resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) controllerShowTimeoutMs = 3000 @@ -145,7 +146,10 @@ fun MediaVideoView( when (event) { Lifecycle.Event.ON_RESUME -> exoPlayer.play() Lifecycle.Event.ON_PAUSE -> exoPlayer.pause() - Lifecycle.Event.ON_DESTROY -> exoPlayer.release() + Lifecycle.Event.ON_DESTROY -> { + exoPlayer.release() + exoPlayer.removeListener(playerListener) + } else -> Unit } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/exoplayer/ExoPlayerWrapper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/exoplayer/ExoPlayerWrapper.kt new file mode 100644 index 0000000000..a69db1ef2c --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/exoplayer/ExoPlayerWrapper.kt @@ -0,0 +1,49 @@ +/* + * 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.local.exoplayer + +import android.content.Context +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer + +/** + * Wrapper around ExoPlayer to disable some commands. + * Necessary to hide the settings wheels from the player. + */ +@UnstableApi +class ExoPlayerWrapper(private val exoPlayer: ExoPlayer) : ExoPlayer by exoPlayer { + + override fun isCommandAvailable(command: Int): Boolean { + return availableCommands.contains(command) + } + + override fun getAvailableCommands(): Player.Commands { + return exoPlayer.availableCommands + .buildUpon() + .remove(Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS) + .build() + } + + companion object { + fun create(context: Context): ExoPlayer { + return ExoPlayerWrapper( + ExoPlayer.Builder(context).build() + ) + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt index b2957cac18..ae598688d1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -17,6 +17,9 @@ package io.element.android.features.messages.impl.media.viewer +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -116,28 +119,33 @@ private fun ThumbnailView( showThumbnail: Boolean, showProgress: Boolean, ) { - if (!showThumbnail) return - - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center + AnimatedVisibility( + visible = showThumbnail, + enter = fadeIn(), + exit = fadeOut() ) { - val mediaRequestData = MediaRequestData( - source = thumbnailSource, - kind = MediaRequestData.Kind.Content - ) - AsyncImage( + Box( modifier = Modifier.fillMaxSize(), - model = mediaRequestData, - contentScale = ContentScale.Fit, - contentDescription = null, - ) - if (showProgress) { - Box( - modifier = Modifier.roundedBackground(), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator() + contentAlignment = Alignment.Center + ) { + val mediaRequestData = MediaRequestData( + source = thumbnailSource, + kind = MediaRequestData.Kind.Content + ) + AsyncImage( + modifier = Modifier.fillMaxSize(), + model = mediaRequestData, + alpha = 0.8f, + contentScale = ContentScale.Fit, + contentDescription = null, + ) + if (showProgress) { + Box( + modifier = Modifier.roundedBackground(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } } } } From ec4c511c66908bf46af58c648dd9b6ce86eb1c58 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 26 May 2023 12:16:14 +0200 Subject: [PATCH 39/48] Media : Fix small issues after merge --- .../android/features/messages/impl/timeline/TimelineView.kt | 2 -- .../impl/timeline/components/event/TimelineItemContentView.kt | 2 +- .../messages/impl/timeline/groups/TimelineItemGrouper.kt | 4 ++++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 65abc98071..40be750c14 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -311,13 +311,11 @@ fun TimelineItemStateEventRow( .zIndex(-1f) .widthIn(max = 320.dp) ) { - val contentModifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) TimelineItemEventContentView( content = event.content, interactionSource = interactionSource, onClick = onClick, onLongClick = onLongClick, - modifier = contentModifier ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt index 9c9fb17c5f..2bb6056d35 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt @@ -77,7 +77,7 @@ fun TimelineItemEventContentView( ) is TimelineItemStateContent -> TimelineItemStateView( content = content, - modifier = modifier + modifier = modifier.defaultContentPadding() ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt index 3cd1b8fd4a..24f74f4b5c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline.groups import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemNoticeContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent @@ -27,6 +28,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.libraries.core.bool.orFalse import kotlinx.collections.immutable.toImmutableList @@ -66,6 +68,8 @@ class TimelineItemGrouper @Inject constructor() { is TimelineItemEmoteContent, is TimelineItemNoticeContent, is TimelineItemTextContent, + is TimelineItemFileContent, + is TimelineItemVideoContent, TimelineItemUnknownContent -> false is TimelineItemProfileChangeContent, is TimelineItemRoomMembershipContent, From 2832c95c69dfc801636b8855733c11196ba8aace Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 26 May 2023 12:20:51 +0200 Subject: [PATCH 40/48] Media: update screenshots again --- ...Group_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 --- ...roup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 --- 24 files changed, 44 insertions(+), 50 deletions(-) delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index b11864d276..84913d3fb7 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:562e83b7771ff2209c7882180af55a8f3123c12c020b294858b1b46bff8a8a95 -size 30928 +oid sha256:b625f841a45111d58d76fd290cfad376a58e16265cc0b0d8e00318ffa1e3f48e +size 35204 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index a55c0f8cdc..66225f1c42 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79b16cbbbf36e6de4ea6270476371c7097424b26d4066f7156738c084fe061de -size 42758 +oid sha256:a5391a08d36198d28cc2c05c83757d30dc207dd56ada9684cd3a3ea7f9812b0a +size 46977 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png index 1cf35816aa..443a04f4e1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:898b99d1603073bd8a047c5ab4c40ae221b88ed3167028d613fbff8e0907a84b -size 40601 +oid sha256:4b00e07786abd487a90069cb0fd3554cb1ebcda18ffb09cd879b6264365d7ade +size 45245 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png index 5db287217d..1efd8082fb 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae648857da2faa4af355759b149cdb251f8f8f9563d0dd2d7543c5431f756454 -size 62756 +oid sha256:8cda4043ba49b469e6e705333dc6bf37db379ab02cdbfd2adef416643d35b7a6 +size 67214 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png index f1bd1303ce..776728bf2a 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7534d8379a1ab220833b8c877f9b3021a665cdb97c71ba383695276d83757702 -size 32721 +oid sha256:6f951f69dd21d36338ddbfb303d50c554ef915c40e57919f699d83afce507d75 +size 36906 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png index 2fa40047c6..e5a622f671 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3123fce3d4e4ed0480d2757e4daa70ecd14ddad1e8471523638561cccbab8dc -size 44568 +oid sha256:4dec759835366e66beae953d11e73a52717500b30d5c58df6e3912a3cc60f916 +size 48704 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png index d0b80fcd37..328ffb4a69 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32d8032b1b2b3ae2b2795dea60b6395104df24790450f9452bbe82e81d9e62e8 -size 28911 +oid sha256:3bebe664eb8cd7a0578977ad998ddfe24283a309a6830cc420416c99b00f1e60 +size 33114 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png index c14452b104..af164ce007 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abff6db46d39ec39f3c0d5366f91252b58cbf62e6850bc86a6e81e52c80ba096 -size 45979 +oid sha256:9ed02fce151bfb9eec7b380e9469b67c8ce9757eb7a0b7ebbcf0a5932c55087e +size 50072 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 3123c5dd15..4113c9a338 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34469e4b86a441fb6f7cfbdce4b622109c093a234ae5d35d895dce35ef10c20c -size 30327 +oid sha256:73de3dd7281ea437f3cb364cca175cec5379046da19fd8f1152f3a52f43b6bfa +size 34442 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 6d5c269d4c..2781e8027e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11c677ab3124a94f531f1db14eb2b2f19a493470620f7375cf7c1d14056cdbc9 -size 42429 +oid sha256:4bb36a52495fc17de86c0b66c6ac5eb8d4df64e3c512db41ec546b02c8157133 +size 46507 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png index 66eb095367..785cf70826 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:878976e75768bf17e753b343ef887f344d523d819c9b8adb804fc7c380827d8c -size 40395 +oid sha256:efaf767e5b45aad6bdbf54cc895b2f06c1cc1eb145a9644aa2e1d7d67f1060e5 +size 44954 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png index b37616000b..008a9a011a 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12519c3717f0281e2d303b1c9c63725ebae55a1a9d503d8e4a63d95375d6dcc5 -size 63441 +oid sha256:94bbbd907515d69854633be5e65bcf0e2891e146c269a04f0274bb35b016f72f +size 67916 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png index 3b8db29b08..17545e89f6 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a35878a3d306b1a6d1768669904ee1f9e0a71abd4ab3c04ff6564027732614f -size 32077 +oid sha256:b668eb7b9ce26c8d4b25d9e955ded21ef40c9db496a81cca99f299c84838fcd3 +size 36143 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png index 816518f7aa..08e023fa8d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1b3b0b538dafa85849c6e255bcff6b1a82a52d49be1fe4ded9b7f7c2ec06bff -size 44431 +oid sha256:3ce9ae983222355419305a239dae4deefa36cf90b03840b69401a3e9329ad4df +size 48421 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png index 25f8d272a1..5404d4ef7e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef18369400f738e94c5a812dc0ec4c8473659eafb11d7361385647ba6a640d15 -size 28320 +oid sha256:7692c7dd3ce432abda585d3e539ab03eb51f1df7debca4b14ba015cfd0111e42 +size 32372 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png index 49caf143da..57c5b61d46 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:633210082fc83be3628250fa35768969aad136035924667bd2f3c6c8307fd87c -size 45641 +oid sha256:ffccef423c7be0a6bcb15facabb174de1f20833aa73af0db7397dd3eb011642e +size 49791 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 580da9ceb6..27584b85d5 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01f54909964a4ed07d8850ab2bffad8b99ed641d731c1808b04f164ff3e0cbba -size 39632 +oid sha256:71ead784370bd7007dc52c48d31651512721c42fdcd37a177cb078802848412d +size 43813 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index ff59a95d62..c326a2eafa 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ea39fdf1cb61657bc1eafb7d353ee15517c26c0e6bb2804b7c8350fa651299e -size 41508 +oid sha256:e29a3b27b84b1be439b6b80e2a6df9ef30c87d74b51ca1079c683fd8d935d2c1 +size 44308 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index e6d1c48263..24bdb3250c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aeec498cb7f1c2210085b5ae7456d0b211ed35abbebffe959e24921c278566da -size 39568 +oid sha256:61ad59bb8d4cffe01fc8b0cd1b315b1129ca51185b3cfe3c730932b3052b6e5a +size 43556 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index e24e462642..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9f535800616d0b9a03fd00088a823bc58b359c0c20c311816de824279308fee3 -size 40267 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 34de0f83a1..8c3dff21f5 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd32f8da6c0547f0cea178d50b281d02d651bdaa49d80143a7a01b9f4cfb429c -size 38654 +oid sha256:e03fa1605d99013550a1bfc705b04fd70fee8c620fee15bfabb1c2ed1829b042 +size 42709 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index c4d2e0d1b4..11eb7de483 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfb73e70d2b438521a11560587080af0adf0e43553806bc35cdb64b3c7944fde -size 40708 +oid sha256:abf236791895e30e444122a59519334ef5b117f54ed45f15be694b64ba57e7e1 +size 43459 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 121379617e..e98a75b5eb 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e8b8baa1425214daebf83f60cceeba8ac63be28da9d9949ea36e1f2e044e2ea -size 37533 +oid sha256:8d27d9bd487ad9d86da6ffc107c39d9bac96836e6a44c5a994e701c5df2535d7 +size 41631 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index 75c9b207a1..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b5bcc73a77a86083a2229b174122ec76d0fdf07973f838c26862e9ece4990311 -size 39133 From 405a6073c6a0b2446690cd80dc26b172ded753c0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 26 May 2023 13:07:27 +0200 Subject: [PATCH 41/48] Fix lint issue --- .../notifications/factories/action/QuickReplyActionFactory.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt index a5f72acfb9..b8e3b28dad 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/action/QuickReplyActionFactory.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.push.impl.notifications.factories.action +import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -70,6 +71,8 @@ class QuickReplyActionFactory @Inject constructor( * However, for Android devices running Marshmallow and below (API level 23 and below), * it will be more appropriate to use an activity. Since you have to provide your own UI. */ + //TODO remove when minSdk will be back to 23 + @SuppressLint("ObsoleteSdkInt") private fun buildQuickReplyIntent( sessionId: SessionId, roomId: RoomId, From fc464a35f90313ebc80a99663a98aa4e9b63a9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 26 May 2023 14:28:37 +0200 Subject: [PATCH 42/48] Try to improve timestamp rendering for media --- .../messages/impl/timeline/TimelineView.kt | 144 ++++++++++++------ .../event/TimelineItemContentView.kt | 16 +- 2 files changed, 101 insertions(+), 59 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 41989fc665..4ba691eba1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl.timeline import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -33,11 +34,13 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDownward import androidx.compose.material.icons.filled.Error @@ -51,6 +54,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.LastBaseline import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -70,8 +74,11 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingModel import io.element.android.libraries.core.bool.orFalse @@ -234,6 +241,7 @@ fun TimelineItemEventRow( modifier: Modifier = Modifier ) { val interactionSource = remember { MutableInteractionSource() } + val (parentAlignment, contentAlignment) = if (event.isMine) { Pair(Alignment.CenterEnd, Alignment.End) } else { @@ -271,25 +279,7 @@ fun TimelineItemEventRow( .zIndex(-1f) .widthIn(max = 320.dp) ) { - Column { - TimelineItemEventContentView( - content = event.content, - interactionSource = interactionSource, - onClick = onClick, - onLongClick = onLongClick, - ) - TimestampView( - formattedTime = event.sentTime, - hasMessageSendingFailed = event.sendState is EventSendState.SendingFailed, - isMessageEdited = (event.content as? TimelineItemTextBasedContent)?.isEdited.orFalse(), - onClick = { - // TODO trigger either resending the message or opening the message edition history. This will be implemented later - }, - modifier = Modifier - .padding(horizontal = 8.dp, vertical = 4.dp) - .align(Alignment.End), - ) - } + MessageEventLayout(event = event, onMessageClick = onClick, onMessageLongClick = onLongClick) } TimelineItemReactionsView( reactionsState = event.reactionsState, @@ -310,6 +300,92 @@ fun TimelineItemEventRow( } } +@Composable +fun MessageEventLayout( + event: TimelineItem.Event, + onMessageClick: () -> Unit, + onMessageLongClick: () -> Unit, + modifier: Modifier = Modifier +) { + val isTextBasedMessage = event.content is TimelineItemTextBasedContent + val isMediaMessage = event.content is TimelineItemImageContent + || event.content is TimelineItemVideoContent + || event.content is TimelineItemFileContent + val isStateMessage = event.content is TimelineItemStateContent + + val interactionSource = remember { MutableInteractionSource() } + + @Composable + fun ContentView( + modifier: Modifier = Modifier + ) { + TimelineItemEventContentView( + content = event.content, + interactionSource = interactionSource, + onClick = onMessageClick, + onLongClick = onMessageLongClick, + modifier = modifier, + ) + } + + @Composable + fun TimestampView( + modifier: Modifier = Modifier + ) { + val formattedTime = event.sentTime + val hasMessageSendingFailed = event.sendState is EventSendState.SendingFailed + val isMessageEdited = (event.content as? TimelineItemTextBasedContent)?.isEdited.orFalse() + val tint = if (hasMessageSendingFailed) ElementTheme.colors.textActionCritical else null + Row(modifier = modifier.clickable(onClick = onMessageClick)) { + if (isMessageEdited) { + Text( + stringResource(StringR.string.common_edited_suffix), + style = ElementTextStyles.Regular.caption2, + color = tint ?: MaterialTheme.colorScheme.secondary, + ) + Spacer(modifier = Modifier.width(4.dp)) + } + Text( + formattedTime, + style = ElementTextStyles.Regular.caption2, + color = tint ?: MaterialTheme.colorScheme.secondary, + ) + if (hasMessageSendingFailed && tint != null) { + Spacer(modifier = Modifier.width(2.dp)) + Icon(imageVector = Icons.Default.Error, contentDescription = "Error sending message", tint = tint, modifier = Modifier.size(15.dp, 18.dp)) + } + } + } + + when { + isMediaMessage -> { + Box(modifier.wrapContentSize()) { + ContentView() + Box( + modifier = Modifier + .wrapContentSize() + .padding(horizontal = 4.dp, vertical = 2.dp) + .background(Color(0xFFF0F2F5), RoundedCornerShape(10.0.dp)) // TODO: add the color with its dark variant + .align(Alignment.BottomEnd) + ) { + TimestampView(Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) + } + } + } + isTextBasedMessage -> { + Column { + ContentView(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 6.dp)) + TimestampView(modifier = Modifier + .align(Alignment.End) + .padding(horizontal = 8.dp, vertical = 4.dp)) + } + } + isStateMessage -> { + ContentView(modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)) + } + } +} + @Composable fun TimelineItemStateEventRow( event: TimelineItem.Event, @@ -344,36 +420,6 @@ fun TimelineItemStateEventRow( } } -@Composable -private fun TimestampView( - formattedTime: String, - isMessageEdited: Boolean, - hasMessageSendingFailed: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - val tint = if (hasMessageSendingFailed) ElementTheme.colors.textActionCritical else null - Row(modifier = modifier.clickable(onClick = onClick)) { - if (isMessageEdited) { - Text( - stringResource(StringR.string.common_edited_suffix), - style = ElementTextStyles.Regular.caption2, - color = tint ?: MaterialTheme.colorScheme.secondary, - ) - Spacer(modifier = Modifier.width(4.dp)) - } - Text( - formattedTime, - style = ElementTextStyles.Regular.caption1, - color = tint ?: MaterialTheme.colorScheme.secondary, - ) - if (hasMessageSendingFailed && tint != null) { - Spacer(modifier = Modifier.width(2.dp)) - Icon(imageVector = Icons.Default.Error, contentDescription = "Error sending message", tint = tint, modifier = Modifier.size(15.dp, 18.dp)) - } - } -} - @Composable private fun MessageSenderInformation( sender: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt index 2bb6056d35..735c4e8106 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt @@ -31,10 +31,6 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent -private fun Modifier.defaultContentPadding(): Modifier = padding( - horizontal = 12.dp, vertical = 6.dp -) - @Composable fun TimelineItemEventContentView( content: TimelineItemEventContent, @@ -46,22 +42,22 @@ fun TimelineItemEventContentView( when (content) { is TimelineItemEncryptedContent -> TimelineItemEncryptedView( content = content, - modifier = modifier.defaultContentPadding() + modifier = modifier ) is TimelineItemRedactedContent -> TimelineItemRedactedView( content = content, - modifier = modifier.defaultContentPadding() + modifier = modifier ) is TimelineItemTextBasedContent -> TimelineItemTextView( content = content, interactionSource = interactionSource, - modifier = modifier.defaultContentPadding(), + modifier = modifier, onTextClicked = onClick, onTextLongClicked = onLongClick ) is TimelineItemUnknownContent -> TimelineItemUnknownView( content = content, - modifier = modifier.defaultContentPadding() + modifier = modifier ) is TimelineItemImageContent -> TimelineItemImageView( content = content, @@ -73,11 +69,11 @@ fun TimelineItemEventContentView( ) is TimelineItemFileContent -> TimelineItemFileView( content = content, - modifier = modifier.defaultContentPadding() + modifier = modifier ) is TimelineItemStateContent -> TimelineItemStateView( content = content, - modifier = modifier.defaultContentPadding() + modifier = modifier ) } } From 15ff1a4d30bd2bf4942d8982dc7f0486ea2b71c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Fri, 26 May 2023 14:37:39 +0200 Subject: [PATCH 43/48] Timestamp improvements --- .../features/messages/impl/timeline/TimelineView.kt | 9 +++++---- .../io/element/android/libraries/designsystem/Color.kt | 6 ++++-- .../android/libraries/designsystem/theme/ColorsDark.kt | 6 ++++-- .../android/libraries/designsystem/theme/ColorsLight.kt | 6 ++++-- .../libraries/designsystem/theme/ElementColors.kt | 7 +++++++ 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 4ba691eba1..c2e60302be 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -87,7 +87,9 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.ElementColors import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.LocalColors import io.element.android.libraries.designsystem.theme.components.FloatingActionButton import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text @@ -363,12 +365,11 @@ fun MessageEventLayout( ContentView() Box( modifier = Modifier - .wrapContentSize() - .padding(horizontal = 4.dp, vertical = 2.dp) - .background(Color(0xFFF0F2F5), RoundedCornerShape(10.0.dp)) // TODO: add the color with its dark variant + .padding(horizontal = 4.dp, vertical = 4.dp) + .background(LocalColors.current.gray300, RoundedCornerShape(10.0.dp)) .align(Alignment.BottomEnd) ) { - TimestampView(Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) + TimestampView(Modifier.padding(horizontal = 4.dp, vertical = 2.dp)) } } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Color.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Color.kt index 60f355d054..50585f9dba 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Color.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/Color.kt @@ -75,5 +75,7 @@ val LinkColor = Color(0xFF0086E6) val TextColorCriticalLight = Color(0xFFD51928) val TextColorCriticalDark = Color(0xfffd3e3c) -val Gray_400_Light = Color(0xFFE1E6EC) -val Gray_400_Dark = Color(0xFF26282D) +val Compound_Gray_300_Light = Color(0xFFF0F2F5) +val Compound_Gray_300_Dark = Color(0xFF1D1F24) +val Compound_Gray_400_Light = Color(0xFFE1E6EC) +val Compound_Gray_400_Dark = Color(0xFF26282D) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsDark.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsDark.kt index e697ed782c..166a2f3f79 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsDark.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsDark.kt @@ -23,10 +23,11 @@ import androidx.compose.ui.tooling.preview.Preview import io.element.android.libraries.designsystem.Azure import io.element.android.libraries.designsystem.Black_800 import io.element.android.libraries.designsystem.Black_950 +import io.element.android.libraries.designsystem.Compound_Gray_300_Dark import io.element.android.libraries.designsystem.DarkGrey import io.element.android.libraries.designsystem.Gray_300 import io.element.android.libraries.designsystem.Gray_400 -import io.element.android.libraries.designsystem.Gray_400_Dark +import io.element.android.libraries.designsystem.Compound_Gray_400_Dark import io.element.android.libraries.designsystem.Gray_450 import io.element.android.libraries.designsystem.SystemGrey5Dark import io.element.android.libraries.designsystem.SystemGrey6Dark @@ -39,7 +40,8 @@ fun elementColorsDark() = ElementColors( messageHighlightedBackground = Azure, quaternary = Gray_400, quinary = Gray_450, - gray400 = Gray_400_Dark, + gray300 = Compound_Gray_300_Dark, + gray400 = Compound_Gray_400_Dark, textActionCritical = TextColorCriticalDark, isLight = false, ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsLight.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsLight.kt index 35fdcf29b1..085dd534cd 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsLight.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorsLight.kt @@ -26,7 +26,8 @@ import io.element.android.libraries.designsystem.Gray_100 import io.element.android.libraries.designsystem.Gray_150 import io.element.android.libraries.designsystem.Gray_200 import io.element.android.libraries.designsystem.Gray_25 -import io.element.android.libraries.designsystem.Gray_400_Light +import io.element.android.libraries.designsystem.Compound_Gray_300_Light +import io.element.android.libraries.designsystem.Compound_Gray_400_Light import io.element.android.libraries.designsystem.Gray_50 import io.element.android.libraries.designsystem.SystemGrey5Light import io.element.android.libraries.designsystem.SystemGrey6Light @@ -39,7 +40,8 @@ fun elementColorsLight() = ElementColors( messageHighlightedBackground = Azure, quaternary = Gray_100, quinary = Gray_50, - gray400 = Gray_400_Light, + gray300 = Compound_Gray_300_Light, + gray400 = Compound_Gray_400_Light, textActionCritical = TextColorCriticalLight, isLight = true, ) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementColors.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementColors.kt index b275d66dd5..2643e678d3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementColors.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementColors.kt @@ -29,6 +29,7 @@ class ElementColors( messageHighlightedBackground: Color, quaternary: Color, quinary: Color, + gray300: Color, gray400: Color, textActionCritical: Color, isLight: Boolean @@ -46,6 +47,9 @@ class ElementColors( var quinary by mutableStateOf(quinary) private set + var gray300 by mutableStateOf(gray400) + private set + var gray400 by mutableStateOf(gray400) private set @@ -61,6 +65,7 @@ class ElementColors( messageHighlightedBackground: Color = this.messageHighlightedBackground, quaternary: Color = this.quaternary, quinary: Color = this.quinary, + gray300: Color = this.gray300, gray400: Color = this.gray400, textActionCritical: Color = this.textActionCritical, isLight: Boolean = this.isLight, @@ -70,6 +75,7 @@ class ElementColors( messageHighlightedBackground = messageHighlightedBackground, quaternary = quaternary, quinary = quinary, + gray300 = gray300, gray400 = gray400, textActionCritical = textActionCritical, isLight = isLight, @@ -81,6 +87,7 @@ class ElementColors( messageHighlightedBackground = other.messageHighlightedBackground quaternary = other.quaternary quinary = other.quinary + gray300 = other.gray300 gray400 = other.gray400 textActionCritical = other.textActionCritical isLight = other.isLight From c72ecc4807606e277da5d8d24debb07c0dbd2f67 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 26 May 2023 17:10:34 +0200 Subject: [PATCH 44/48] Media: clean a bit Timestamp rendering --- .../messages/impl/timeline/TimelineView.kt | 160 +++++++----------- .../components/TimelineEventTimestampView.kt | 70 ++++++++ .../messages/impl/timeline/util/Modifiers.kt | 23 +++ 3 files changed, 155 insertions(+), 98 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/Modifiers.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index c2e60302be..0f31652ec8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -18,7 +18,6 @@ package io.element.android.features.messages.impl.timeline import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -43,7 +42,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDownward -import androidx.compose.material.icons.filled.Error import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -54,10 +52,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.LastBaseline import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -65,6 +61,7 @@ import androidx.compose.ui.zIndex import io.element.android.features.messages.impl.R import io.element.android.features.messages.impl.timeline.components.MessageEventBubble import io.element.android.features.messages.impl.timeline.components.MessageStateEventContainer +import io.element.android.features.messages.impl.timeline.components.TimelineEventTimestampView import io.element.android.features.messages.impl.timeline.components.TimelineItemReactionsView import io.element.android.features.messages.impl.timeline.components.event.TimelineItemEventContentView import io.element.android.features.messages.impl.timeline.components.group.GroupHeaderView @@ -74,30 +71,23 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingModel -import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.designsystem.ElementTextStyles +import io.element.android.features.messages.impl.timeline.util.defaultTimelineContentPadding import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.theme.ElementColors -import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.designsystem.theme.LocalColors import io.element.android.libraries.designsystem.theme.components.FloatingActionButton import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -import io.element.android.libraries.ui.strings.R as StringR @Composable fun TimelineView( @@ -281,7 +271,12 @@ fun TimelineItemEventRow( .zIndex(-1f) .widthIn(max = 320.dp) ) { - MessageEventLayout(event = event, onMessageClick = onClick, onMessageLongClick = onLongClick) + MessageEventBubbleContent( + event = event, + interactionSource = interactionSource, + onMessageClick = onClick, + onMessageLongClick = onLongClick + ) } TimelineItemReactionsView( reactionsState = event.reactionsState, @@ -302,91 +297,6 @@ fun TimelineItemEventRow( } } -@Composable -fun MessageEventLayout( - event: TimelineItem.Event, - onMessageClick: () -> Unit, - onMessageLongClick: () -> Unit, - modifier: Modifier = Modifier -) { - val isTextBasedMessage = event.content is TimelineItemTextBasedContent - val isMediaMessage = event.content is TimelineItemImageContent - || event.content is TimelineItemVideoContent - || event.content is TimelineItemFileContent - val isStateMessage = event.content is TimelineItemStateContent - - val interactionSource = remember { MutableInteractionSource() } - - @Composable - fun ContentView( - modifier: Modifier = Modifier - ) { - TimelineItemEventContentView( - content = event.content, - interactionSource = interactionSource, - onClick = onMessageClick, - onLongClick = onMessageLongClick, - modifier = modifier, - ) - } - - @Composable - fun TimestampView( - modifier: Modifier = Modifier - ) { - val formattedTime = event.sentTime - val hasMessageSendingFailed = event.sendState is EventSendState.SendingFailed - val isMessageEdited = (event.content as? TimelineItemTextBasedContent)?.isEdited.orFalse() - val tint = if (hasMessageSendingFailed) ElementTheme.colors.textActionCritical else null - Row(modifier = modifier.clickable(onClick = onMessageClick)) { - if (isMessageEdited) { - Text( - stringResource(StringR.string.common_edited_suffix), - style = ElementTextStyles.Regular.caption2, - color = tint ?: MaterialTheme.colorScheme.secondary, - ) - Spacer(modifier = Modifier.width(4.dp)) - } - Text( - formattedTime, - style = ElementTextStyles.Regular.caption2, - color = tint ?: MaterialTheme.colorScheme.secondary, - ) - if (hasMessageSendingFailed && tint != null) { - Spacer(modifier = Modifier.width(2.dp)) - Icon(imageVector = Icons.Default.Error, contentDescription = "Error sending message", tint = tint, modifier = Modifier.size(15.dp, 18.dp)) - } - } - } - - when { - isMediaMessage -> { - Box(modifier.wrapContentSize()) { - ContentView() - Box( - modifier = Modifier - .padding(horizontal = 4.dp, vertical = 4.dp) - .background(LocalColors.current.gray300, RoundedCornerShape(10.0.dp)) - .align(Alignment.BottomEnd) - ) { - TimestampView(Modifier.padding(horizontal = 4.dp, vertical = 2.dp)) - } - } - } - isTextBasedMessage -> { - Column { - ContentView(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 6.dp)) - TimestampView(modifier = Modifier - .align(Alignment.End) - .padding(horizontal = 8.dp, vertical = 4.dp)) - } - } - isStateMessage -> { - ContentView(modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)) - } - } -} - @Composable fun TimelineItemStateEventRow( event: TimelineItem.Event, @@ -416,6 +326,60 @@ fun TimelineItemStateEventRow( interactionSource = interactionSource, onClick = onClick, onLongClick = onLongClick, + modifier = Modifier.defaultTimelineContentPadding() + ) + } + } +} + +@Composable +fun MessageEventBubbleContent( + event: TimelineItem.Event, + interactionSource: MutableInteractionSource, + onMessageClick: () -> Unit, + onMessageLongClick: () -> Unit, + modifier: Modifier = Modifier +) { + val showTimestampWithOverlay = event.content is TimelineItemImageContent || event.content is TimelineItemVideoContent + + @Composable + fun ContentView( + modifier: Modifier = Modifier + ) { + TimelineItemEventContentView( + content = event.content, + interactionSource = interactionSource, + onClick = onMessageClick, + onLongClick = onMessageLongClick, + modifier = modifier, + ) + } + + if (showTimestampWithOverlay) { + Box(modifier.wrapContentSize()) { + ContentView() + Box( + modifier = Modifier + .padding(horizontal = 4.dp, vertical = 4.dp) + .background(LocalColors.current.gray300, RoundedCornerShape(10.0.dp)) + .align(Alignment.BottomEnd) + ) { + TimelineEventTimestampView( + event = event, + onClick = onMessageClick, + modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp) + ) + } + } + } else { + Column { + ContentView(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 8.dp)) + TimelineEventTimestampView( + event = event, + onClick = onMessageClick, + modifier = Modifier + .align(Alignment.End) + .padding(horizontal = 8.dp, vertical = 2.dp) ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt new file mode 100644 index 0000000000..8358d6cc1e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent +import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.designsystem.ElementTextStyles +import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState +import io.element.android.libraries.ui.strings.R + +@Composable +fun TimelineEventTimestampView( + event: TimelineItem.Event, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val formattedTime = event.sentTime + val hasMessageSendingFailed = event.sendState is EventSendState.SendingFailed + val isMessageEdited = (event.content as? TimelineItemTextBasedContent)?.isEdited.orFalse() + val tint = if (hasMessageSendingFailed) ElementTheme.colors.textActionCritical else null + Row(modifier = modifier.clickable(onClick = onClick)) { + if (isMessageEdited) { + Text( + stringResource(R.string.common_edited_suffix), + style = ElementTextStyles.Regular.caption2, + color = tint ?: MaterialTheme.colorScheme.secondary, + ) + Spacer(modifier = Modifier.width(4.dp)) + } + Text( + formattedTime, + style = ElementTextStyles.Regular.caption2, + color = tint ?: MaterialTheme.colorScheme.secondary, + ) + if (hasMessageSendingFailed && tint != null) { + Spacer(modifier = Modifier.width(2.dp)) + Icon(imageVector = Icons.Default.Error, contentDescription = "Error sending message", tint = tint, modifier = Modifier.size(15.dp, 18.dp)) + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/Modifiers.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/Modifiers.kt new file mode 100644 index 0000000000..611e270742 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/util/Modifiers.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.timeline.util + +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +fun Modifier.defaultTimelineContentPadding() = padding(horizontal = 12.dp, vertical = 6.dp) From b2a66ddafec9d899facfebb6f9b8350060ca6db4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 26 May 2023 19:09:51 +0200 Subject: [PATCH 45/48] Media: handle PR review remarks --- .../element/android/features/messages/impl/MessagesView.kt | 2 +- .../messages/impl/media/viewer/MediaViewerPresenter.kt | 6 +++--- .../android/libraries/matrix/api/media/MatrixMediaLoader.kt | 2 +- .../android/libraries/matrix/impl/media/RustMediaLoader.kt | 2 +- .../android/libraries/matrix/test/media/FakeMediaLoader.kt | 2 +- .../android/libraries/matrix/ui/media/CoilMediaFetcher.kt | 3 +-- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 7ec51dab3f..cdaa27cc52 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -105,7 +105,7 @@ fun MessagesView( initialValue = ModalBottomSheetValue.Hidden, ) val composerState = state.composerState - val initialBottomSheetState = if (LocalInspectionMode.current && composerState.showAttachmentSourcePicker != null) { + val initialBottomSheetState = if (LocalInspectionMode.current && composerState.showAttachmentSourcePicker) { ModalBottomSheetValue.Expanded } else { ModalBottomSheetValue.Hidden diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt index 01b562f62c..fb563461c5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt @@ -58,7 +58,7 @@ class MediaViewerPresenter @AssistedInject constructor( mutableStateOf(Async.Uninitialized) } DisposableEffect(loadMediaTrigger) { - coroutineScope.loadMedia(mediaFile, localMedia) + coroutineScope.downloadMedia(mediaFile, localMedia) onDispose { mediaFile.value?.close() } @@ -80,9 +80,9 @@ class MediaViewerPresenter @AssistedInject constructor( ) } - private fun CoroutineScope.loadMedia(mediaFile: MutableState, localMedia: MutableState>) = launch { + private fun CoroutineScope.downloadMedia(mediaFile: MutableState, localMedia: MutableState>) = launch { localMedia.value = Async.Loading() - mediaLoader.loadMediaFile(inputs.mediaSource, inputs.mimeType) + mediaLoader.downloadMediaFile(inputs.mediaSource, inputs.mimeType) .onSuccess { mediaFile.value = it }.mapCatching { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt index 5dabef6d48..4d1d2445ce 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt @@ -36,5 +36,5 @@ interface MatrixMediaLoader { * @param mimeType: optional mime type * @return a [Result] of [MediaFile] */ - suspend fun loadMediaFile(source: MediaSource, mimeType: String?): Result + suspend fun downloadMediaFile(source: MediaSource, mimeType: String?): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt index 5e9c26d6c6..9e4f2c53de 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt @@ -59,7 +59,7 @@ class RustMediaLoader( } } - override suspend fun loadMediaFile(source: MediaSource, mimeType: String?): Result = + override suspend fun downloadMediaFile(source: MediaSource, mimeType: String?): Result = withContext(dispatchers.io) { runCatching { source.toRustMediaSource().use { mediaSource -> diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt index 266c81c605..96c49aa165 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt @@ -44,7 +44,7 @@ class FakeMediaLoader : MatrixMediaLoader { } } - override suspend fun loadMediaFile(source: MediaSource, mimeType: String?): Result { + override suspend fun downloadMediaFile(source: MediaSource, mimeType: String?): Result { delay(FAKE_DELAY_IN_MS) return if (shouldFail) { Result.failure(RuntimeException()) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt index a409b9de97..d638db2902 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt @@ -35,8 +35,7 @@ internal class CoilMediaFetcher( override suspend fun fetch(): FetchResult? { return loadMedia() .map { data -> - ByteBuffer.wrap(data) - }.map { byteBuffer -> + val byteBuffer = ByteBuffer.wrap(data) imageLoader.components.newFetcher(byteBuffer, options, imageLoader)?.first?.fetch() }.getOrThrow() } From fce45b828c48b9ad6e7ae0f94672ac980e9f04e3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 26 May 2023 19:13:51 +0200 Subject: [PATCH 46/48] Media: generate again screenshots... --- ...Group_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png | 3 +++ ...Group_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...oup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png | 3 +++ ...roup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 --- ...roup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 --- 30 files changed, 58 insertions(+), 58 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 660e7604ab..ac79e58737 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bd4c1be83610c41da8e95d5a2bcd141e513f62ca7223075256149b8c7bb9d25 -size 41915 +oid sha256:0e7e30c460d75815023ed02cd6b0fcff4a15881916f0056e47d9fd034bf3a181 +size 41125 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 636c9994b1..8ca1e75687 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:180265714b5b0a370899468d9ba0f08e4e7d5d3af0721585ad579a643c00c04a -size 53963 +oid sha256:81b126a3d322d70f06db15622e560a146cea2c81f5873987550aba0a5c5efffa +size 53314 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2a2d363120 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7e8b8ff0aeeb517c47dfd987579b73a7e862e9a67a5100532f39087d42a4fe4 +size 46056 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index da0a9aa4f3..cbe31a3f09 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3ae864ab2a0856e73c6457dc12f9f7e59842befb5f3c983348c34cf70d4672d -size 255 +oid sha256:b0ede4470399e2c7e6f3c99e59191a857c1e14254789307777359802980c59a1 +size 193735 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png index 909569cc8f..809b1bba99 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:deaebc55f67c36ecf75f0947868247425596f53866669f77d3cf0c55a0003632 -size 255 +oid sha256:c9fe22528aad68a5dffc8489fd1ec3138a8edde44fb23a1391f6e53a07d44fff +size 193966 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png index 29ffbfa6e3..fc4cbf27fd 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d981d2fa2ce67605954b388768c8587bc8c402ce9d7bf1a26d53a3354891f1eb -size 254 +oid sha256:d559a454af6dfe367d1256dcd7ceeb32ae6fe168b76b1b5d8860d328e9752fb3 +size 51433 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png index d7fb489b8f..cd486afaf0 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c4cfe45d9cd9ab10134f664fd9bf18ac42de2d5f81acd7e2580f3f42b52bd3d -size 254 +oid sha256:1e45ec17c6353f96f0f777f8c638da010dbd062c680175d8ae80f6e700a6f928 +size 72941 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png index 90ecd219d0..bd287148b7 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80021de1e90d971e486d7ee2c8e2e92124a044d98f85cdb1292e434bfcd5c9f9 -size 254 +oid sha256:572d6f11354e2b1f02cb0979b35e2c798abe2d3b63f852ca6b868471ceecaed0 +size 42964 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png index e5a622f671..842ea61125 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4dec759835366e66beae953d11e73a52717500b30d5c58df6e3912a3cc60f916 -size 48704 +oid sha256:b25f0497c08515125896756c89c5bc9e1757b25ec50478cd5cb67cdfd00c2dec +size 55019 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png index 328ffb4a69..d83002fedd 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bebe664eb8cd7a0578977ad998ddfe24283a309a6830cc420416c99b00f1e60 -size 33114 +oid sha256:3f22479bb2c6bd35c6f927c11ff19e644226df3d2a99b9c6b23f5bd679178d5a +size 39151 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png index af164ce007..cc3b898a92 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ed02fce151bfb9eec7b380e9469b67c8ce9757eb7a0b7ebbcf0a5932c55087e -size 50072 +oid sha256:6920b50647111a70215c74ca27be736d6c208c0801eb4bcd323abcb33e903aea +size 56229 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 410bcded08..8a2c64bd25 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02f7a5ee81caf8fb81af1f2d5a4e98dfadf59e279a7814e177f657a68400110e -size 41256 +oid sha256:a01bde6db3ce00e84e9bb5d6a8179c8d97af9dde939f6e31aa81c955dbdbf3a3 +size 40586 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 4202b21562..a07f4eb8ed 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f761a4cb835fdf706588d33463989dca4fd944282a24c2e78cf4c166032d8b53 -size 53778 +oid sha256:3f12a13dd0fb3b569ce332ebc4e00fb55a6402bbf2b2a927b02abb4056c5e5c3 +size 53043 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bdb8e46417 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c50bb17bf530de6472d44232b0248c2a5761c67fde785b3aee6eb3f77fa4103e +size 45948 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 7805a60bad..cdfd4fd6be 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eaeee0605ce741603cacd878cd970e15616f065247fce432dbf6fb4177e665ff -size 255 +oid sha256:c0f6c785a1e20160355c72356744dedba65bda3d453fbd9772390350eaf6cc6e +size 195504 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png index 788282a88b..cb6574b5ad 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8c2a116793562fbf0b3987649f2c7d21a47524620c7f88d1b840e4c07bab828 -size 255 +oid sha256:37c702936170a0e4b37d608cf62b92c3b60b93950d4e9f32633af77e3ce4fdc9 +size 195775 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png index 3f053b6b7a..949cd3ef67 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b06f34095698894e909f967d16dec59353a77dc34899848212ab525de747ca65 -size 254 +oid sha256:e0b885810087916d3fefe699e5abc5125138ad74c6377f560e010e7fe68892da +size 51388 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png index 0f200369cc..6a5e4d9ec6 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2338ae31e82089a95863240fcf287551391ce8247b7726e344fb8a3c23e4b2bc -size 254 +oid sha256:adcb71f4b9639287beabf35c9e91837bebee8f6b070fa77c0e6520fdf24faaae +size 73679 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png index 68f64e83c2..07823941c7 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18ead89bcaa95b6d321ee8068f2690b3513d9bc35bab00de569f8924cbf5a3e8 -size 254 +oid sha256:cb24bbba2e17417f4f3459353ee2c762517acfae2eada90c47b559828bad16a0 +size 42411 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png index 08e023fa8d..61aaf14dac 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ce9ae983222355419305a239dae4deefa36cf90b03840b69401a3e9329ad4df -size 48421 +oid sha256:d75c877c55c0f622aabee5ed8428b8a980cbc28daacb2127f2a17cc29db62a9d +size 55057 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png index 5404d4ef7e..97d47f37f0 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7692c7dd3ce432abda585d3e539ab03eb51f1df7debca4b14ba015cfd0111e42 -size 32372 +oid sha256:a6edb33e04054aba49ea978d6b2c0d0c25332c4dd901cf2cadd6c368076e957d +size 38687 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png index 57c5b61d46..4bca8aaed3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffccef423c7be0a6bcb15facabb174de1f20833aa73af0db7397dd3eb011642e -size 49791 +oid sha256:996016191220540fde3d8aaa3124a074a6eaaf6e87c43bf0d57d32992faf47fc +size 56342 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 09d47863c5..3884544dd1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49f7df48099566151474d1a3183dcc72359cf0b6a9bb5d38b15bd86ead6f9fde -size 44390 +oid sha256:9c2c642f1595f503a1deb1c241f14543d7f15221baa1d0937595aa023f36a874 +size 44765 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 7d61515e2d..0dd33fab8e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51f47e0b2ed979001dd76e440f8620f90ac42410999565a4341dad96bc5ecfbc -size 44924 +oid sha256:1afd2c42012017f154fdf031d4a5fc7384fa5f239b56cf4567f42fd579806c9a +size 45363 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 10b43fe4da..a934e0c73e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d44b17653c344047225850811f3a5423fb3aca8c03a3925710c2e1df1a795d92 -size 254 +oid sha256:9481f2994885a7649aa54d1c0043c44fe390004a7b58db4fc9d0e3e70bfc258a +size 43537 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index c4b0eaa7bc..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d8cb1f4a11abbd47e86e575165e15e79f3e9385b7876e2348949a5553237b5f -size 40671 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index ad9b64589d..a32dc0d376 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbdbec55388b60790188bf6d178b87cc52211cc1c5f62f3af8fe32bbe40ed6ae -size 254 +oid sha256:220b9ac66b74724887d7b0bbc082b4d0240d9fc25a068d33491eb83fe76e45fd +size 43958 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index eed7e14d59..6c69a3850e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ae6fe2834dc220fd474599e6aec1d43f18d35ca43924cb421d4db11b9fdb14f -size 254 +oid sha256:9aa83ed14fb23495e79500a1e7d9dee8215efe67eec89e65e382d4f695eac68b +size 44871 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 280b865cd3..0be594f38f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2a252bf1df88f74ec25847517d6fb10dc82c94724655ef5c6b982b2222962ad -size 254 +oid sha256:aa4970f995c6ee618be3d868ff5a34442352a7f4493cad345093b889daf0d4c2 +size 42042 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index 2c1d31e9c0..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ea5e469f454f50b88bc2d2860f3271119b1b85bacd8afe7210cb8ce38b44252c -size 40019 From 698e08573bb2b27744a9c7c1a7e539cf8ec65ce4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 26 May 2023 19:21:12 +0200 Subject: [PATCH 47/48] Media: fix test compilation --- .../features/messages/media/viewer/MediaViewerPresenterTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt index a828356586..17c9a012ad 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt @@ -93,7 +93,8 @@ class MediaViewerPresenterTest { inputs = MediaViewerNode.Inputs( name = TESTED_MEDIA_NAME, mediaSource = aMediaSource(), - mimeType = mimeType + mimeType = mimeType, + thumbnailSource = null ), localMediaFactory = localMediaFactory, mediaLoader = mediaLoader From 87990c425cd16f90423d40fdc014ae2ba19ce7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 29 May 2023 13:58:29 +0200 Subject: [PATCH 48/48] Fix small issues, improve `Result.flatMap` --- .../features/messages/impl/MessagesView.kt | 8 ++- .../impl/media/local/LocalMediaFactory.kt | 2 - .../messages/MessagesPresenterTest.kt | 3 +- .../media/viewer/MediaViewerPresenterTest.kt | 2 +- .../libraries/core/extensions/Result.kt | 27 +++++--- .../libraries/core/extensions/ResultTests.kt | 69 +++++++++++++++++++ .../libraries/mediaupload/api/MediaSender.kt | 2 +- 7 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/ResultTests.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index cdaa27cc52..e3c6160b20 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -357,5 +357,11 @@ internal fun MessagesViewDarkPreview(@PreviewParameter(MessagesStateProvider::cl @Composable private fun ContentToPreview(state: MessagesState) { - MessagesView(state, {}, {}, {}, {}) + MessagesView( + state = state, + onBackPressed = {}, + onRoomDetailsClicked = {}, + onEventClicked = {}, + onPreviewAttachments = {} + ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt index 581461de7f..09c44f4fba 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt @@ -23,14 +23,12 @@ interface LocalMediaFactory { /** * This method will create a [LocalMedia] with the given [MediaFile] and [mimeType]. - * */ fun createFromMediaFile(mediaFile: MediaFile, mimeType: String?): LocalMedia /** * This method will create a [LocalMedia] with the given [uri] and [mimeType] * If the [mimeType] is null, it'll try to read it from the content. - * */ fun createFromUri(uri: Uri, mimeType: String?): LocalMedia } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 14807e3c7c..1d8ee54505 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -26,10 +26,10 @@ import io.element.android.features.messages.impl.MessagesEvents import io.element.android.features.messages.impl.MessagesPresenter import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction -import io.element.android.features.messages.media.FakeLocalMediaFactory import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper +import io.element.android.features.messages.media.FakeLocalMediaFactory import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.featureflag.test.FakeFeatureFlagService @@ -156,4 +156,3 @@ class MessagesPresenterTest { ) } } - diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt index 17c9a012ad..16f2894303 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt @@ -51,7 +51,7 @@ class MediaViewerPresenterTest { assertThat(initialState.name).isEqualTo(TESTED_MEDIA_NAME) val loadingState = awaitItem() assertThat(loadingState.downloadedMedia).isInstanceOf(Async.Loading::class.java) - testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS) + testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS + 1) val successState = awaitItem() val successData = successState.downloadedMedia.dataOrNull() assertThat(successState.downloadedMedia).isInstanceOf(Async.Success::class.java) diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt index 3ddd4f9105..f7d96aebf5 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/Result.kt @@ -27,14 +27,23 @@ inline fun Result.mapFailure(transform: (exception: Throwable) -> } /** - * Can be used to transform some Throwable into some other. + * Can be used to apply a [transform] that returns a [Result] to a base [Result] and get another [Result]. + * @return The result of the transform as a [Result]. */ -inline fun Result.flatMap(transform: (R) -> Result): Result { - return when (val exception = exceptionOrNull()) { - null -> mapCatching(transform).fold( - onSuccess = { it }, - onFailure = { Result.failure(it) } - ) - else -> Result.failure(exception) - } +inline fun Result.flatMap(transform: (T) -> Result): Result { + return map(transform).fold( + onSuccess = { it }, + onFailure = { Result.failure(it) } + ) +} + +/** + * Can be used to apply a [transform] that returns a [Result] to a base [Result] and get another [Result], catching any exception. + * @return The result of the transform or a caught exception wrapped in a [Result]. + */ +inline fun Result.flatMapCatching(transform: (T) -> Result): Result { + return mapCatching(transform).fold( + onSuccess = { it }, + onFailure = { Result.failure(it) } + ) } diff --git a/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/ResultTests.kt b/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/ResultTests.kt new file mode 100644 index 0000000000..70a45c9011 --- /dev/null +++ b/libraries/core/src/test/kotlin/io/element/android/libraries/core/extensions/ResultTests.kt @@ -0,0 +1,69 @@ +/* + * 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.core.extensions + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ResultTests { + + @Test + fun testFlatMap() { + val initial = Result.success("initial") + val otherResult = initial.flatMap { Result.success("other") } + val errorResult = initial.flatMap { Result.failure(IllegalStateException("error")) } + + assertThat(otherResult.getOrNull()).isEqualTo("other") + assertThat(errorResult.exceptionOrNull()?.message).isEqualTo("error") + try { + initial.flatMap { error("caught error") } + } catch (e: IllegalStateException) { + assertThat(e.message).isEqualTo("caught error") + } + + val initialError = Result.failure(IllegalStateException("initial error")) + val mapErrorToSuccess = initialError.flatMap { Result.success("other") } + val mapErrorToError = initialError.flatMap { Result.failure(IllegalStateException("error")) } + val mapErrorAndCatch: Result = initialError.flatMap { error("error") } + + assertThat(mapErrorToSuccess.exceptionOrNull()?.message).isEqualTo("initial error") + assertThat(mapErrorToError.exceptionOrNull()?.message).isEqualTo("initial error") + assertThat(mapErrorAndCatch.exceptionOrNull()?.message).isEqualTo("initial error") + } + + @Test + fun testFlatMapCatching() { + val initial = Result.success("initial") + val otherResult = initial.flatMapCatching { Result.success("other") } + val errorResult = initial.flatMapCatching { Result.failure(IllegalStateException("error")) } + val caughtExceptionResult: Result = initial.flatMapCatching { error("caught error") } + + assertThat(otherResult.getOrNull()).isEqualTo("other") + assertThat(errorResult.exceptionOrNull()?.message).isEqualTo("error") + assertThat(caughtExceptionResult.exceptionOrNull()?.message).isEqualTo("caught error") + + val initialError = Result.failure(IllegalStateException("initial error")) + val mapErrorToSuccess = initialError.flatMapCatching { Result.success("other") } + val mapErrorToError = initialError.flatMapCatching { Result.failure(IllegalStateException("error")) } + val mapErrorAndCatch: Result = initialError.flatMapCatching { error("error") } + + assertThat(mapErrorToSuccess.exceptionOrNull()?.message).isEqualTo("initial error") + assertThat(mapErrorToError.exceptionOrNull()?.message).isEqualTo("initial error") + assertThat(mapErrorAndCatch.exceptionOrNull()?.message).isEqualTo("initial error") + } + +} diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index d08860d4ca..585670d939 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -54,7 +54,7 @@ class MediaSender @Inject constructor( is MediaUploadInfo.AnyFile -> { sendFile(info.file, info.info) } - else -> error("Unexpected MediaUploadInfo format: $info") + else -> Result.failure(IllegalStateException("Unexpected MediaUploadInfo format: $info")) } } }