[Message actions] New UI for replies (#545)

* Add 'reply to' UI to the message composer.

* Move the `BlurHashAsyncImage` to `:libraries:designsystem` as it is now used in several modules.

*  Create reusable `AttachmentThumbnail` and associated data classes and enums, it's now added to `:libraries:matrixui`.

* Re-use `AttachmentThumbnail` in a `ActionListView` and `TextComposer`.

* Add 'inReplyTo' models and UI.

* Add min size for images

* Create a separate layout for media items with no reply to info. Also, separate `Timeline__Row` components from `TimelineView`, as it was getting too large.

* Added `EqualWidthColumn` to use inside message bubbles. Also fixed some modifiers for media items replying to other messages.

* Disable `inReplyToClicked`.

* Remove unused resources and libraries.

* Remove any traces of `BlurHashAsyncImage` in `:features:messages`, since it was moved to the design system.

---------

Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2023-06-08 12:15:13 +02:00 committed by GitHub
parent 43e4e1627d
commit 25c32cb1e8
56 changed files with 1253 additions and 1008 deletions

View file

@ -0,0 +1,100 @@
/*
* 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.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.AsyncImage
import com.vanniktech.blurhash.BlurHash
@Composable
fun BlurHashAsyncImage(
model: Any?,
blurHash: String?,
modifier: Modifier = Modifier,
contentScale: ContentScale = ContentScale.Fit,
contentDescription: String? = null,
) {
var isLoading by rememberSaveable(model) { mutableStateOf(true) }
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
AsyncImage(
modifier = Modifier.fillMaxSize(),
model = model,
contentScale = contentScale,
contentDescription = contentDescription,
onSuccess = { isLoading = false }
)
AnimatedVisibility(
visible = isLoading,
enter = fadeIn(),
exit = fadeOut(),
) {
BlurHashImage(
blurHash = blurHash,
contentDescription = contentDescription,
contentScale = ContentScale.FillBounds,
)
}
}
}
@Composable
fun BlurHashImage(
blurHash: String?,
modifier: Modifier = Modifier,
contentDescription: String? = null,
contentScale: ContentScale = ContentScale.Fit,
) {
if (blurHash == null) return
val bitmapState = remember(blurHash) {
mutableStateOf(
// Build a small blurhash image so that it's fast
BlurHash.decode(blurHash, 10, 10)
)
}
DisposableEffect(blurHash) {
onDispose {
bitmapState.value?.recycle()
}
}
bitmapState.value?.let { bitmap ->
Image(
modifier = modifier.fillMaxSize(),
bitmap = bitmap.asImageBitmap(),
contentScale = contentScale,
contentDescription = contentDescription
)
}
}

View file

@ -0,0 +1,58 @@
/*
* 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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
/**
* Used to create a column where all children have the same width.
* It will first measure all children, get the largest width and re-measure all children with this width as the minWidth.
*
* *Note*: If all children already have the same width, it skips the 2nd measuring and acts like a normal Column.
*/
@Composable
fun EqualWidthColumn(
modifier: Modifier = Modifier,
spacing: Dp = 0.dp,
content: @Composable () -> Unit
) {
SubcomposeLayout(modifier = modifier) { constraints ->
val measurables = subcompose(0, content).map { it.measure(constraints) }
val maxWidth = measurables.maxOf { it.width }
val newConstraints = constraints.copy(minWidth = maxWidth)
val newMeasurables = if (measurables.all { it.width == maxWidth }) {
// Skip re-measuring if all children have the same width
measurables
} else {
// Re-measure with the largest width as the minWidth to have all children constrained to the same width
subcompose(1, content).map { it.measure(newConstraints) }
}
val totalHeight = (newMeasurables.sumOf { it.height } + spacing.toPx() * (newMeasurables.size - 1)).roundToInt()
layout(maxWidth, totalHeight) {
var yPosition = 0
newMeasurables.forEach { measurable ->
measurable.placeRelative(0, yPosition)
yPosition += measurable.height + spacing.roundToPx()
}
}
}
}