Merge pull request #6150 from element-hq/feature/fga/space_ui_tweaks

Iterate on Space related UI
This commit is contained in:
ganfra 2026-02-10 11:36:09 +01:00 committed by GitHub
commit b271e06973
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
176 changed files with 695 additions and 583 deletions

View file

@ -0,0 +1,83 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.designsystem.atomic.atoms
import android.content.ClipData
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.toClipEntry
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
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.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.toDp
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.launch
@Composable
fun RoomPreviewAliasAtom(
alias: String,
modifier: Modifier = Modifier,
copiable: Boolean = true
) {
val clipboard: Clipboard = LocalClipboard.current
val coroutineScope = rememberCoroutineScope()
Row(
modifier = modifier
.clickable(enabled = copiable) {
coroutineScope.launch {
val clipData = ClipData.newPlainText(alias, alias)
clipboard.setClipEntry(clipData.toClipEntry())
}
},
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(weight = 1f, fill = false),
text = alias,
style = ElementTheme.typography.fontBodyLgRegular,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = ElementTheme.colors.textSecondary,
)
if (copiable) {
Icon(
imageVector = CompoundIcons.Copy(),
contentDescription = stringResource(id = CommonStrings.action_copy),
tint = ElementTheme.colors.iconSecondaryAlpha,
modifier = Modifier.size(ElementTheme.typography.fontBodyLgRegular.fontSize.toDp())
)
}
}
}
@PreviewsDayNight
@Composable
internal fun RoomPreviewAliasAtomPreview() = ElementPreview {
RoomPreviewAliasAtom(
alias = "#room-alias:matrix.org",
copiable = true
)
}

View file

@ -1,26 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.designsystem.atomic.atoms
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun RoomPreviewSubtitleAtom(subtitle: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = subtitle,
style = ElementTheme.typography.fontBodyLgRegular,
textAlign = TextAlign.Center,
color = ElementTheme.colors.textSecondary,
)
}

View file

@ -9,6 +9,7 @@
package io.element.android.libraries.designsystem.atomic.organisms
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -19,12 +20,12 @@ import androidx.compose.ui.unit.dp
@Composable
fun RoomPreviewOrganism(
avatar: @Composable () -> Unit,
title: @Composable () -> Unit,
subtitle: @Composable () -> Unit,
avatar: @Composable ColumnScope.() -> Unit,
title: @Composable ColumnScope.() -> Unit,
subtitle: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
description: @Composable (() -> Unit)? = null,
memberCount: @Composable (() -> Unit)? = null,
description: @Composable (ColumnScope.() -> Unit)? = null,
memberCount: @Composable (ColumnScope.() -> Unit)? = null,
) {
Column(
modifier = modifier.fillMaxWidth(),

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.designsystem.components.avatar.internal
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
@ -16,6 +17,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
@ -34,19 +36,26 @@ internal fun SpaceAvatar(
contentDescription: String? = null,
) {
val size = forcedAvatarSize ?: avatarData.size.dp
val avatarShape = avatarType.avatarShape(size)
val commonModifier = modifier
.border(
width = 1.dp,
color = ElementTheme.colors.iconQuaternaryAlpha,
shape = avatarShape,
)
when {
avatarType.isTombstoned -> TombstonedRoomAvatar(
size = size,
avatarShape = avatarType.avatarShape(size),
modifier = modifier,
avatarShape = avatarShape,
modifier = commonModifier,
contentDescription = contentDescription,
)
else -> InitialOrImageAvatar(
avatarData = avatarData,
hideAvatarImage = hideAvatarImage,
avatarShape = avatarType.avatarShape(size),
avatarShape = avatarShape,
forcedAvatarSize = forcedAvatarSize,
modifier = modifier,
modifier = commonModifier,
contentDescription = contentDescription,
)
}

View file

@ -14,12 +14,12 @@ import io.element.android.libraries.matrix.api.room.join.JoinRule
sealed interface SpaceRoomVisibility {
data object Private : SpaceRoomVisibility
data object Public : SpaceRoomVisibility
data object Restricted : SpaceRoomVisibility
data object SpaceMembers : SpaceRoomVisibility
companion object {
fun fromJoinRule(joinRule: JoinRule?): SpaceRoomVisibility = when (joinRule) {
JoinRule.Public -> Public
is JoinRule.Restricted, is JoinRule.KnockRestricted -> Restricted
is JoinRule.Restricted, is JoinRule.KnockRestricted -> SpaceMembers
// Else fallback to Private
else -> Private
}

View file

@ -9,13 +9,17 @@
package io.element.android.libraries.matrix.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
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.text.font.FontStyle
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewAliasAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewTitleAtom
import io.element.android.libraries.designsystem.atomic.organisms.RoomPreviewOrganism
@ -26,6 +30,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.spaces.SpaceRoomVisibility
import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.ui.strings.CommonStrings
@ -39,6 +44,7 @@ import kotlinx.collections.immutable.persistentListOf
fun SpaceHeaderView(
avatarData: AvatarData,
name: String?,
alias: RoomAlias?,
topic: String?,
visibility: SpaceRoomVisibility,
heroes: ImmutableList<MatrixUser>,
@ -66,7 +72,15 @@ fun SpaceHeaderView(
}
},
subtitle = {
SpaceInfoRow(visibility = visibility)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
if (alias != null) {
RoomPreviewAliasAtom(alias = alias.value)
}
SpaceInfoRow(visibility = visibility)
}
},
description = if (topic.isNullOrBlank()) {
null
@ -100,6 +114,7 @@ internal fun SpaceHeaderViewPreview() = ElementPreview {
url = "anUrl",
size = AvatarSize.SpaceHeader,
),
alias = RoomAlias("#spaceAlias:matrix.org"),
name = "Space name",
topic = "Space topic: " + LoremIpsum(40).values.first(),
topicMaxLines = 2,

View file

@ -117,7 +117,7 @@ internal fun SpaceInfoRowPreview() = ElementPreview {
visibility = SpaceRoomVisibility.Public
)
SpaceInfoRow(
visibility = SpaceRoomVisibility.Restricted
visibility = SpaceRoomVisibility.SpaceMembers
)
}
}

View file

@ -24,11 +24,9 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@ -100,22 +98,16 @@ fun SpaceRoomItemView(
showIndicator = showUnreadIndicator
)
Spacer(modifier = Modifier.height(1.dp))
SubtitleRow(
visibilityIcon = spaceRoom.visibilityIcon(),
subtitle = spaceRoom.subtitle()
)
VisibilityRow(visibility = spaceRoom.visibility)
Spacer(modifier = Modifier.height(1.dp))
val info = spaceRoom.info()
if (info.isNotBlank()) {
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyMdRegular,
text = info,
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyMdRegular,
text = pluralStringResource(CommonPlurals.common_member_count, spaceRoom.numJoinedMembers, spaceRoom.numJoinedMembers),
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (bottomAction != null) {
Spacer(modifier = Modifier.height(12.dp))
@ -129,29 +121,26 @@ fun SpaceRoomItemView(
}
@Composable
private fun SubtitleRow(
visibilityIcon: ImageVector?,
subtitle: String,
private fun VisibilityRow(
visibility: SpaceRoomVisibility,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
if (visibilityIcon != null) {
Icon(
modifier = Modifier
.size(16.dp)
.padding(end = 4.dp),
imageVector = visibilityIcon,
contentDescription = null,
tint = ElementTheme.colors.iconTertiary,
)
}
Icon(
modifier = Modifier
.size(16.dp)
.padding(end = 4.dp),
imageVector = visibility.icon,
contentDescription = null,
tint = ElementTheme.colors.iconTertiary,
)
Text(
modifier = Modifier.weight(1f),
style = ElementTheme.typography.fontBodyMdRegular,
text = subtitle,
text = visibility.label,
color = ElementTheme.colors.textSecondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
@ -219,36 +208,6 @@ private fun SpaceRoomItemScaffold(
}
}
@Composable
@ReadOnlyComposable
private fun SpaceRoom.subtitle(): String {
return if (isSpace) {
visibility.label
} else {
pluralStringResource(CommonPlurals.common_member_count, numJoinedMembers, numJoinedMembers)
}
}
@Composable
@ReadOnlyComposable
private fun SpaceRoom.info(): String {
return if (isSpace) {
pluralStringResource(CommonPlurals.common_member_count, numJoinedMembers, numJoinedMembers)
} else {
topic.orEmpty()
}
}
@Composable
private fun SpaceRoom.visibilityIcon(): ImageVector? {
// Don't show any icon for restricted rooms as it's the default and would add noise
return if (visibility == SpaceRoomVisibility.Restricted) {
null
} else {
visibility.icon
}
}
@Composable
@PreviewsDayNight
internal fun SpaceRoomItemViewPreview(@PreviewParameter(SpaceRoomProvider::class) spaceRoom: SpaceRoom) = ElementPreview {

View file

@ -32,7 +32,7 @@ val SpaceRoomVisibility.icon: ImageVector
return when (this) {
SpaceRoomVisibility.Private -> CompoundIcons.LockSolid()
SpaceRoomVisibility.Public -> CompoundIcons.Public()
SpaceRoomVisibility.Restricted -> CompoundIcons.Space()
SpaceRoomVisibility.SpaceMembers -> CompoundIcons.Space()
}
}
@ -41,8 +41,8 @@ val SpaceRoomVisibility.label: String
@ReadOnlyComposable
get() {
return when (this) {
SpaceRoomVisibility.Private -> stringResource(CommonStrings.common_private_space)
SpaceRoomVisibility.Public -> stringResource(CommonStrings.common_public_space)
SpaceRoomVisibility.Restricted -> stringResource(CommonStrings.common_shared_space)
SpaceRoomVisibility.Private -> stringResource(CommonStrings.common_private)
SpaceRoomVisibility.Public -> stringResource(CommonStrings.common_public)
SpaceRoomVisibility.SpaceMembers -> stringResource(CommonStrings.common_space_members)
}
}