Merge branch 'develop' into feature/fga/room_navigation

This commit is contained in:
ganfra 2024-04-10 16:55:55 +02:00
commit 73f276ba8e
447 changed files with 3318 additions and 1374 deletions

View file

@ -34,6 +34,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.R
import io.element.android.libraries.designsystem.modifiers.blurCompat
import io.element.android.libraries.designsystem.modifiers.blurredShapeShadow
@ -171,6 +172,7 @@ internal fun ElementLogoAtomLargeNoBlurShadowPreview() = ElementPreview {
ContentToPreview(ElementLogoAtomSize.Large, useBlurredShadow = false)
}
@ExcludeFromCoverage
@Composable
private fun ContentToPreview(elementLogoAtomSize: ElementLogoAtomSize, useBlurredShadow: Boolean = true) {
Box(

View file

@ -59,8 +59,8 @@ fun InfoListItemMolecule(
color = backgroundColor,
shape = backgroundShape,
)
.padding(vertical = 12.dp, horizontal = 20.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
.padding(vertical = 12.dp, horizontal = 18.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
icon()
message()

View file

@ -34,6 +34,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
/**
* @param modifier Classical modifier.
* @param background optional background component.
* @param topBar optional topBar.
* @param header optional header.
* @param footer optional footer.
@ -42,6 +43,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun HeaderFooterPage(
modifier: Modifier = Modifier,
background: @Composable () -> Unit = {},
topBar: @Composable () -> Unit = {},
header: @Composable () -> Unit = {},
footer: @Composable () -> Unit = {},
@ -51,25 +53,28 @@ fun HeaderFooterPage(
modifier = modifier,
topBar = topBar,
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
.padding(all = 20.dp),
) {
// Header
header()
// Content
Box {
background()
Column(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
.padding(all = 20.dp)
.padding(padding)
.consumeWindowInsets(padding)
) {
content()
}
// Footer
Box(modifier = Modifier.padding(horizontal = 16.dp)) {
footer()
// Header
header()
// Content
Column(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
) {
content()
}
// Footer
Box(modifier = Modifier.padding(horizontal = 16.dp)) {
footer()
}
}
}
}

View file

@ -0,0 +1,86 @@
/*
* Copyright (c) 2024 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.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.LinearGradientShader
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
/**
* Gradient background for FTUE (onboarding) screens.
*/
@Suppress("ModifierMissing")
@Composable
fun OnboardingBackground() {
Box(modifier = Modifier.fillMaxSize()) {
val isLightTheme = ElementTheme.isLightTheme
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(220.dp)
.align(Alignment.BottomCenter)
) {
val gradientBrush = ShaderBrush(
LinearGradientShader(
from = Offset(0f, size.height / 2f),
to = Offset(size.width, size.height / 2f),
colors = listOf(
Color(0xFF0DBDA8),
if (isLightTheme) Color(0xC90D5CBD) else Color(0xFF0D5CBD),
)
)
)
val eraseBrush = ShaderBrush(
LinearGradientShader(
from = Offset(size.width / 2f, 0f),
to = Offset(size.width / 2f, size.height * 2f),
colors = listOf(
Color(0xFF000000),
Color(0x00000000),
)
)
)
drawWithLayer {
drawRect(brush = gradientBrush, size = size)
drawRect(brush = gradientBrush, size = size, blendMode = BlendMode.Overlay)
drawRect(brush = eraseBrush, size = size, blendMode = BlendMode.DstOut)
}
}
}
}
@PreviewsDayNight
@Composable
internal fun OnboardingBackgroundPreview() {
ElementPreview {
OnboardingBackground()
}
}

View file

@ -23,16 +23,21 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import coil.compose.SubcomposeAsyncImage
import coil.compose.SubcomposeAsyncImageContent
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
@ -71,16 +76,34 @@ private fun ImageAvatar(
modifier: Modifier = Modifier,
contentDescription: String? = null,
) {
AsyncImage(
model = avatarData,
onError = {
Timber.e(it.result.throwable, "Error loading avatar $it\n${it.result}")
},
contentDescription = contentDescription,
contentScale = ContentScale.Crop,
placeholder = debugPlaceholderAvatar(),
modifier = modifier
)
if (LocalInspectionMode.current) {
// For compose previews, use debugPlaceholderAvatar()
// instead of falling back to initials avatar on load failure
AsyncImage(
model = avatarData,
contentDescription = contentDescription,
placeholder = debugPlaceholderAvatar(),
modifier = modifier
)
} else {
SubcomposeAsyncImage(
model = avatarData,
contentDescription = contentDescription,
contentScale = ContentScale.Crop,
modifier = modifier
) {
when (val state = painter.state) {
is AsyncImagePainter.State.Success -> SubcomposeAsyncImageContent()
is AsyncImagePainter.State.Error -> {
SideEffect {
Timber.e(state.result.throwable, "Error loading avatar $state\n${state.result}")
}
InitialsAvatar(avatarData = avatarData)
}
else -> InitialsAvatar(avatarData = avatarData)
}
}
}
}
@Composable

View file

@ -36,6 +36,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.atomic.atoms.RedIndicatorAtom
import io.element.android.libraries.designsystem.components.preferences.components.PreferenceIcon
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
@ -162,6 +163,7 @@ internal fun PreferenceTextWithEndBadgeDarkPreview() = ElementPreviewDark {
ContentToPreview(showEndBadge = true)
}
@ExcludeFromCoverage
@Composable
private fun ContentToPreview(showEndBadge: Boolean) {
Column(

View file

@ -1,107 +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.modifiers
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.updateTransition
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.platform.debugInspectorInfo
import kotlin.math.sqrt
// Note: these modifiers come from https://gist.github.com/darvld/eb3844474baf2f3fc6d3ab44a4b4b5f8
/**
* A modifier that clips the composable content using an animated circle. The circle will
* expand/shrink with an animation whenever [visible] changes.
*
* For more fine-grained control over the transition, see this method's overload, which allows passing
* a [State] object to control the progress of the reveal animation.
*
* By default, the circle is centered in the content, but custom positions may be specified using
* [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).*/
fun Modifier.circularReveal(
visible: Boolean,
showScrim: Boolean = false,
revealFrom: Offset = Offset(0.5f, 0.5f),
): Modifier = composed(
factory = {
val factor = updateTransition(visible, label = "Visibility")
.animateFloat(label = "revealFactor") { if (it) 1f else 0f }
circularReveal(factor, showScrim, revealFrom)
},
inspectorInfo = debugInspectorInfo {
name = "circularReveal"
properties["visible"] = visible
properties["revealFrom"] = revealFrom
}
)
/**
* A modifier that clips the composable content using a circular shape. The radius of the circle
* will be determined by the [transitionProgress].
*
* The values of the progress should be between 0 and 1.
*
* By default, the circle is centered in the content, but custom positions may be specified using
* [revealFrom]. Specified offsets should be between 0 (left/top) and 1 (right/bottom).
* */
fun Modifier.circularReveal(
transitionProgress: State<Float>,
showScrim: Boolean = false,
revealFrom: Offset = Offset(0.5f, 0.5f)
): Modifier {
return drawWithCache {
val path = Path()
val center = revealFrom.mapTo(size)
val radius = calculateRadius(revealFrom, size)
val scrimColor = if (showScrim) {
Color.Gray
} else {
Color.Transparent
}
path.addOval(Rect(center, radius * transitionProgress.value))
onDrawWithContent {
if (showScrim) {
drawRect(scrimColor, alpha = transitionProgress.value * 0.75f)
}
clipPath(path) { this@onDrawWithContent.drawContent() }
}
}
}
private fun Offset.mapTo(size: Size): Offset {
return Offset(x * size.width, y * size.height)
}
private fun calculateRadius(normalizedOrigin: Offset, size: Size) = with(normalizedOrigin) {
val x = (if (x > 0.5f) x else 1 - x) * size.width
val y = (if (y > 0.5f) y else 1 - y) * size.height
sqrt(x * x + y * y)
}

View file

@ -0,0 +1,188 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.modifiers
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreview
import kotlin.math.max
import kotlin.math.min
/**
* Makes the content square in size.
*
* This is achieved by cropping incoming max constraints to the largest possible square size
* and measuring the content using resulting constraints.
* Next the size of layout is decided based on largest dimension of the measured content.
* Finally the content is placed inside the square layout according to specified [position].
*
* If no square exists that falls within the size range of the incoming constraints,
* the content will be laid out as usual, as if the modifier was not applied.
*
* @param position The fraction of the content's position inside its square layout.
* It determines the point on the axis that was extended to make a square.
* Typically you'd want to use values between `0` and `1`, inclusive, where `0`
* will place the content at the "start" of the square, `0.5` in the middle, and `1` at the "end".
*/
@Stable
fun Modifier.squareSize(
position: Float = 0.5f,
): Modifier =
this.then(
when {
position == 0.5f -> SquareSizeCenter
else -> createSquareSizeModifier(position = position)
}
)
private val SquareSizeCenter = createSquareSizeModifier(position = 0.5f)
private class SquareSizeModifier(
private val position: Float,
inspectorInfo: InspectorInfo.() -> Unit,
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val maxSquare = min(constraints.maxWidth, constraints.maxHeight)
val minSquare = max(constraints.minWidth, constraints.minHeight)
val squareExists = minSquare <= maxSquare
val resolvedConstraints = constraints
.takeUnless { squareExists }
?: constraints.copy(maxWidth = maxSquare, maxHeight = maxSquare)
val placeable = measurable.measure(resolvedConstraints)
return if (squareExists) {
val size = max(placeable.width, placeable.height)
layout(size, size) {
val x = ((size - placeable.width) * position).toInt()
val y = ((size - placeable.height) * position).toInt()
placeable.placeRelative(x, y)
}
} else {
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (other !is SquareSizeModifier) return false
if (position != other.position) return false
return true
}
override fun hashCode(): Int {
return position.hashCode()
}
}
@Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
private fun createSquareSizeModifier(
position: Float,
) =
SquareSizeModifier(
position = position,
inspectorInfo = debugInspectorInfo {
name = "squareSize"
properties["position"] = position
},
)
@Preview
@Composable
internal fun SquareSizeModifierLargeWidthPreview() {
ElementPreview {
Box(
modifier = Modifier
.padding(32.dp)
.background(Color.Gray)
.squareSize(position = 0.25f)
) {
Box(
modifier = Modifier
.background(Color.Black)
.size(100.dp, 10.dp)
)
}
}
}
@Preview
@Composable
internal fun SquareSizeModifierLargeHeightPreview() {
ElementPreview {
Box(
modifier = Modifier
.padding(32.dp)
.background(Color.Gray)
.squareSize(position = 0.75f)
) {
Box(
modifier = Modifier
.background(Color.Black)
.size(10.dp, 100.dp)
)
}
}
}
@Preview
@Composable
internal fun SquareSizeModifierInsideSquarePreview() {
ElementPreview {
Box(
modifier = Modifier
.padding(32.dp)
.size(120.dp)
.background(Color.Gray),
contentAlignment = Alignment.Center,
) {
Box(
modifier = Modifier
.background(Color.Black)
.width(100.dp)
.squareSize(position = 0.75f)
)
}
}
}

View file

@ -43,6 +43,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
@ -269,6 +270,7 @@ internal fun SearchBarActiveWithContentPreview() = ElementThemedPreview {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ExcludeFromCoverage
private fun ContentToPreview(
query: String = "",
active: Boolean = false,