Create spaces (#5982)
* Allow creating a space with `CreateRoomParameters` * Add 'Create space' menu item in the spaces home screen. Also, imports new strings related to spaces. * Link the 'Create space' button with the screen to create the space * Unify room access and visibility for `ConfigureRoom`, use the updated design * Fix `EditRoomDetails` avatar size (68dp) * Replace `EditableAvatarView` and `UnsavedAvatar` copmonents with `AvatarPickerView` * `AvatarDataFetcherFactory`: Make sure we use a fallback image fetcher when the URL is not an MXC one (a local one, i.e.). This removes the previous need for a separate `UnsavedAvatarView` * Use `AvatarPickerView` in all the screens where `EditableAvatarView` was used * Improve naming and previews * Update strings, remove unused ones for `RoomAccessItem` * Make `isSpace` part of the `CreateRoomConfig` * Ensure the content fits in the screenshots for `AvatarPickerSizesPreview` * Add `AvatarDataFetcherFactoryTest` * Add new feature flag for creating spaces * Fix ripple being too large for the `Pick` state * Tweak margins and section titles a bit * Add preview for `HomeTopBar` with the spaces case * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
983c012b79
commit
6d1ed5967b
150 changed files with 1097 additions and 778 deletions
|
|
@ -0,0 +1,436 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.components
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
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.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.icons.CompoundDrawables
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
/**
|
||||
* Avatar picker view, based on https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Spaces-on-Element-X?node-id=5918-97417&t=JYDQysgjS33AZb74-4
|
||||
*
|
||||
* It takes a [state], which can be [AvatarPickerState.Pick] for displaying the 'pick avatar' button, or [AvatarPickerState.Selected] when an avatar has
|
||||
* already been selected.
|
||||
*
|
||||
* Note: this function contains lots of 'magic numbers', but those are just the fractions used to scale the different dimensions based on the Figma design.
|
||||
*/
|
||||
@Composable
|
||||
fun AvatarPickerView(
|
||||
state: AvatarPickerState,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (() -> Unit) = {},
|
||||
onClickLabel: String? = stringResource(CommonStrings.a11y_edit_avatar),
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
val a11yAvatar = stringResource(CommonStrings.a11y_avatar)
|
||||
|
||||
val clickableModifier = Modifier.clickable(
|
||||
enabled = enabled,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClickLabel = onClickLabel,
|
||||
onClick = onClick,
|
||||
indication = ripple(bounded = false),
|
||||
)
|
||||
.testTag(TestTags.editAvatar)
|
||||
.clearAndSetSemantics {
|
||||
contentDescription = a11yAvatar
|
||||
}
|
||||
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
|
||||
fun eraseBackgroundModifier(
|
||||
parentWidth: Dp,
|
||||
editIconRadius: Dp,
|
||||
) = Modifier
|
||||
.graphicsLayer {
|
||||
compositingStrategy = CompositingStrategy.Offscreen
|
||||
}
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawCircle(
|
||||
color = Color.Black,
|
||||
center = Offset(
|
||||
x = if (layoutDirection == LayoutDirection.Ltr) {
|
||||
parentWidth.toPx() - editIconRadius.toPx() * 0.48f
|
||||
} else {
|
||||
editIconRadius.toPx() * 0.48f
|
||||
},
|
||||
y = size.height - editIconRadius.toPx(),
|
||||
),
|
||||
radius = editIconRadius.toPx() * 1.2f,
|
||||
blendMode = BlendMode.Clear,
|
||||
)
|
||||
}
|
||||
|
||||
when (state) {
|
||||
is AvatarPickerState.Pick -> {
|
||||
PickButton(
|
||||
buttonSize = state.buttonSize,
|
||||
iconSize = state.iconSize,
|
||||
iconId = state.iconId,
|
||||
modifier = modifier.padding(state.externalPadding).then(clickableModifier),
|
||||
)
|
||||
}
|
||||
is AvatarPickerState.Selected -> {
|
||||
Box(modifier = modifier) {
|
||||
Avatar(
|
||||
avatarData = state.avatarData,
|
||||
avatarType = state.type,
|
||||
modifier = clickableModifier.then(eraseBackgroundModifier(state.avatarData.size.dp, state.avatarData.size.dp * 0.225f)),
|
||||
)
|
||||
|
||||
OverlayEditButton(editButtonSize = state.avatarData.size.dp * 0.44f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PickButton(
|
||||
buttonSize: Dp,
|
||||
iconSize: Dp,
|
||||
@DrawableRes iconId: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(buttonSize)
|
||||
.clip(CircleShape)
|
||||
.border(BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary), shape = CircleShape)
|
||||
) {
|
||||
Icon(
|
||||
resourceId = iconId,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(iconSize),
|
||||
tint = ElementTheme.colors.iconPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.OverlayEditButton(editButtonSize: Dp) {
|
||||
Box(
|
||||
modifier = Modifier.align(Alignment.BottomEnd)
|
||||
.size(editButtonSize)
|
||||
.offset(x = editButtonSize * 0.266f)
|
||||
.clip(CircleShape)
|
||||
.background(ElementTheme.colors.bgCanvasDefault)
|
||||
.border(BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary), shape = CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(editButtonSize * 0.66f),
|
||||
imageVector = CompoundIcons.Edit(),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface AvatarPickerState {
|
||||
data class Pick(
|
||||
val buttonSize: Dp,
|
||||
val iconSize: Dp = buttonSize / 2,
|
||||
val externalPadding: PaddingValues = PaddingValues.Zero,
|
||||
@DrawableRes val iconId: Int = CompoundDrawables.ic_compound_take_photo,
|
||||
) : AvatarPickerState
|
||||
|
||||
data class Selected(
|
||||
val avatarData: AvatarData,
|
||||
val type: AvatarType,
|
||||
) : AvatarPickerState
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun AvatarPickerViewPreview() = ElementPreview {
|
||||
PreviewContent()
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun AvatarPickerViewRtlPreview() = CompositionLocalProvider(
|
||||
LocalLayoutDirection provides LayoutDirection.Rtl,
|
||||
) {
|
||||
ElementPreview { PreviewContent() }
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun AvatarPickerSizesPreview() = ElementPreview {
|
||||
Column {
|
||||
Row {
|
||||
AvatarPickerView(AvatarPickerState.Pick(buttonSize = 24.dp, externalPadding = PaddingValues(6.dp)), onClick = {})
|
||||
AvatarPickerView(AvatarPickerState.Pick(buttonSize = 32.dp, externalPadding = PaddingValues(6.dp)), onClick = {})
|
||||
AvatarPickerView(AvatarPickerState.Pick(buttonSize = 48.dp, externalPadding = PaddingValues(6.dp)), onClick = {})
|
||||
AvatarPickerView(AvatarPickerState.Pick(buttonSize = 64.dp, externalPadding = PaddingValues(6.dp)), onClick = {})
|
||||
AvatarPickerView(AvatarPickerState.Pick(buttonSize = 96.dp, externalPadding = PaddingValues(6.dp)), onClick = {})
|
||||
}
|
||||
Row {
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.TimelineThreadLatestEventSender),
|
||||
type = AvatarType.User
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(6.dp)
|
||||
)
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.ReadReceiptList),
|
||||
type = AvatarType.User
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(6.dp)
|
||||
)
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.SelectedUser),
|
||||
type = AvatarType.User
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(6.dp)
|
||||
)
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.EditRoomDetails),
|
||||
type = AvatarType.User
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(6.dp)
|
||||
)
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.RoomListManageUser),
|
||||
type = AvatarType.User
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(6.dp)
|
||||
)
|
||||
}
|
||||
Row {
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.TimelineThreadLatestEventSender),
|
||||
type = AvatarType.Space()
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(6.dp)
|
||||
)
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.ReadReceiptList),
|
||||
type = AvatarType.Space()
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(6.dp)
|
||||
)
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.SelectedUser),
|
||||
type = AvatarType.Space()
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(6.dp)
|
||||
)
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.EditRoomDetails),
|
||||
type = AvatarType.Space()
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(6.dp)
|
||||
)
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.RoomListManageUser),
|
||||
type = AvatarType.Space()
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(6.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PreviewContent() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("Pick image")
|
||||
AvatarPickerView(AvatarPickerState.Pick(buttonSize = 48.dp, externalPadding = PaddingValues(6.dp)), onClick = {})
|
||||
HorizontalDivider()
|
||||
|
||||
Text("User avatar")
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("No url")
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("@user:example.com", "User", null, size = AvatarSize.EditRoomDetails),
|
||||
type = AvatarType.User
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Local")
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("@user:example.com", "User", "content://test", size = AvatarSize.EditRoomDetails),
|
||||
type = AvatarType.User
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("MXC")
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("@user:example.com", "User", "mxc://test", size = AvatarSize.EditRoomDetails),
|
||||
type = AvatarType.User
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
Text("Room avatar")
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("No url")
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("!room:example.com", "Room", null, size = AvatarSize.EditRoomDetails),
|
||||
type = AvatarType.Room()
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Local")
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("!room:example.com", "Room", "content://test", size = AvatarSize.EditRoomDetails),
|
||||
type = AvatarType.Room()
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("MXC")
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("!room:example.com", "Room", "mxc://test", size = AvatarSize.EditRoomDetails),
|
||||
type = AvatarType.Room()
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
Text("Space avatar")
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("No url")
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("!room:example.com", "Space", null, size = AvatarSize.EditRoomDetails),
|
||||
type = AvatarType.Space()
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Local")
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("!room:example.com", "Space", "content://test", size = AvatarSize.EditRoomDetails),
|
||||
type = AvatarType.Space()
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("MXC")
|
||||
AvatarPickerView(
|
||||
AvatarPickerState.Selected(
|
||||
avatarData = AvatarData("!room:example.com", "Space", "mxc://test", size = AvatarSize.EditRoomDetails),
|
||||
type = AvatarType.Space()
|
||||
),
|
||||
onClick = {},
|
||||
modifier = Modifier.padding(10.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
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.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun EditableAvatarView(
|
||||
matrixId: String,
|
||||
displayName: String?,
|
||||
avatarUrl: String?,
|
||||
avatarSize: AvatarSize,
|
||||
avatarType: AvatarType,
|
||||
onAvatarClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
val a11yAvatar = stringResource(CommonStrings.a11y_avatar)
|
||||
val editIconRadius = 15.dp
|
||||
val parentHeight = avatarSize.dp
|
||||
val parentWidth = avatarSize.dp + editIconRadius / 2f
|
||||
Box(
|
||||
modifier = modifier
|
||||
.wrapContentSize()
|
||||
.size(height = parentHeight, width = parentWidth)
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClickLabel = stringResource(CommonStrings.a11y_edit_avatar),
|
||||
onClick = onAvatarClick,
|
||||
indication = ripple(bounded = false),
|
||||
)
|
||||
.testTag(TestTags.editAvatar)
|
||||
.clearAndSetSemantics {
|
||||
contentDescription = a11yAvatar
|
||||
},
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.graphicsLayer {
|
||||
compositingStrategy = CompositingStrategy.Offscreen
|
||||
}
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
drawCircle(
|
||||
color = Color.Black,
|
||||
center = Offset(
|
||||
x = parentWidth.toPx() - editIconRadius.toPx(),
|
||||
y = size.height - editIconRadius.toPx(),
|
||||
),
|
||||
radius = (editIconRadius + 4.dp).toPx(),
|
||||
blendMode = BlendMode.Clear,
|
||||
)
|
||||
}
|
||||
) {
|
||||
when {
|
||||
avatarUrl == null || avatarUrl.startsWith("mxc://") -> {
|
||||
Avatar(
|
||||
avatarData = AvatarData(
|
||||
id = matrixId,
|
||||
name = displayName,
|
||||
url = avatarUrl,
|
||||
size = avatarSize,
|
||||
),
|
||||
avatarType = avatarType,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
UnsavedAvatar(
|
||||
avatarUri = avatarUrl,
|
||||
avatarSize = avatarSize,
|
||||
avatarType = avatarType,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(editIconRadius * 2)
|
||||
.border(1.dp, ElementTheme.colors.borderInteractiveSecondary, CircleShape)
|
||||
.padding(6.dp),
|
||||
imageVector = CompoundIcons.Edit(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun EditableAvatarViewPreview(
|
||||
@PreviewParameter(EditableAvatarViewUriProvider::class) uri: String?
|
||||
) = ElementPreview(
|
||||
drawableFallbackForImages = CommonDrawables.sample_avatar,
|
||||
) {
|
||||
EditableAvatarView(
|
||||
matrixId = "id",
|
||||
displayName = "Room",
|
||||
avatarUrl = uri,
|
||||
avatarSize = AvatarSize.RoomDetailsHeader,
|
||||
avatarType = AvatarType.User,
|
||||
onAvatarClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
open class EditableAvatarViewUriProvider : PreviewParameterProvider<String?> {
|
||||
override val values: Sequence<String?>
|
||||
get() = sequenceOf(
|
||||
null,
|
||||
"mxc://matrix.org/123456",
|
||||
"https://example.com/avatar.jpg",
|
||||
)
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.AddAPhoto
|
||||
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.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.components.avatar.avatarShape
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial
|
||||
|
||||
/**
|
||||
* An avatar that the user has selected, but which has not yet been uploaded to Matrix.
|
||||
*
|
||||
* The image is loaded from a local resource instead of from a MXC URI.
|
||||
*/
|
||||
@Composable
|
||||
fun UnsavedAvatar(
|
||||
avatarUri: String?,
|
||||
avatarSize: AvatarSize,
|
||||
avatarType: AvatarType,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val commonModifier = modifier
|
||||
.size(avatarSize.dp)
|
||||
.clip(avatarType.avatarShape(avatarSize.dp))
|
||||
|
||||
if (avatarUri != null) {
|
||||
val context = LocalContext.current
|
||||
val model = ImageRequest.Builder(context)
|
||||
.data(avatarUri)
|
||||
.build()
|
||||
AsyncImage(
|
||||
modifier = commonModifier,
|
||||
model = model,
|
||||
placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant),
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null,
|
||||
)
|
||||
} else {
|
||||
Box(modifier = commonModifier.background(ElementTheme.colors.temporaryColorBgSpecial)) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.AddAPhoto,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(avatarSize.dp * 4 / 7),
|
||||
tint = ElementTheme.colors.iconSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun UnsavedAvatarPreview() = ElementPreview {
|
||||
Row(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
UnsavedAvatar(null, AvatarSize.EditRoomDetails, AvatarType.User)
|
||||
UnsavedAvatar("", AvatarSize.EditRoomDetails, AvatarType.User)
|
||||
UnsavedAvatar(null, AvatarSize.EditRoomDetails, AvatarType.Space())
|
||||
UnsavedAvatar("", AvatarSize.EditRoomDetails, AvatarType.Space())
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ fun RoomAddressField(
|
|||
homeserverName: String,
|
||||
addressValidity: RoomAddressValidity,
|
||||
onAddressChange: (String) -> Unit,
|
||||
label: String,
|
||||
label: String?,
|
||||
supportingText: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue