Merge pull request #5207 from element-hq/feature/bma/spaceInfoUi

Add UI components for spaces.
This commit is contained in:
Benoit Marty 2025-08-23 16:16:39 +02:00 committed by GitHub
commit 4653164572
18 changed files with 288 additions and 26 deletions

View file

@ -65,5 +65,6 @@ enum class AvatarSize(val dp: Dp) {
UserVerification(52.dp),
OrganizationHeader(64.dp),
SpaceHeader(64.dp),
SpaceMember(24.dp),
}

View file

@ -7,8 +7,10 @@
package io.element.android.libraries.matrix.api.room.join
import androidx.compose.runtime.Immutable
import io.element.android.libraries.matrix.api.core.RoomId
@Immutable
sealed interface AllowRule {
data class RoomMembership(val roomId: RoomId) : AllowRule
data class Custom(val json: String) : AllowRule

View file

@ -7,12 +7,16 @@
package io.element.android.libraries.matrix.api.room.join
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
@Immutable
sealed interface JoinRule {
data object Public : JoinRule
data object Private : JoinRule
data object Knock : JoinRule
data object Invite : JoinRule
data class Restricted(val rules: List<AllowRule>) : JoinRule
data class KnockRestricted(val rules: List<AllowRule>) : JoinRule
data class Restricted(val rules: ImmutableList<AllowRule>) : JoinRule
data class KnockRestricted(val rules: ImmutableList<AllowRule>) : JoinRule
data class Custom(val value: String) : JoinRule
}

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.impl.room.join
import io.element.android.libraries.matrix.api.room.join.JoinRule
import kotlinx.collections.immutable.toPersistentList
import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule
fun RustJoinRule.map(): JoinRule {
@ -16,9 +17,9 @@ fun RustJoinRule.map(): JoinRule {
RustJoinRule.Private -> JoinRule.Private
RustJoinRule.Knock -> JoinRule.Knock
RustJoinRule.Invite -> JoinRule.Invite
is RustJoinRule.Restricted -> JoinRule.Restricted(rules.map { it.map() })
is RustJoinRule.Restricted -> JoinRule.Restricted(rules.map { it.map() }.toPersistentList())
is RustJoinRule.Custom -> JoinRule.Custom(repr)
is RustJoinRule.KnockRestricted -> JoinRule.KnockRestricted(rules.map { it.map() })
is RustJoinRule.KnockRestricted -> JoinRule.KnockRestricted(rules.map { it.map() }.toPersistentList())
}
}

View file

@ -16,8 +16,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
@ -28,8 +26,6 @@ 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.ui.strings.CommonPlurals
import io.element.android.libraries.ui.strings.CommonStrings
/**
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=3643-2048&m=dev
@ -60,16 +56,9 @@ fun OrganizationHeader(
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(12.dp))
val subtitle = stringResource(
id = CommonStrings.screen_space_list_details,
pluralStringResource(CommonPlurals.common_spaces, numberOfSpaces, numberOfSpaces),
pluralStringResource(CommonPlurals.common_rooms, numberOfRooms, numberOfRooms),
)
Text(
text = subtitle,
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
SpaceInfoRow(
leftText = numberOfSpaces(numberOfSpaces),
rightText = numberOfRooms(numberOfRooms),
)
}
}

View file

@ -0,0 +1,108 @@
/*
* Copyright 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.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
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.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.room.join.JoinRule
import io.element.android.libraries.matrix.api.user.MatrixUser
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=3643-2429&m=dev
*/
@Composable
fun SpaceHeaderView(
avatarData: AvatarData,
name: String,
topic: String,
joinRule: JoinRule,
heroes: ImmutableList<MatrixUser>,
numberOfMembers: Long,
numberOfRooms: Int,
modifier: Modifier = Modifier,
topicMaxLines: Int = Int.MAX_VALUE,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(top = 32.dp, bottom = 24.dp, start = 16.dp, end = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Avatar(
avatarData = avatarData,
avatarType = AvatarType.Space(false),
)
Text(
text = name,
style = ElementTheme.typography.fontHeadingLgBold,
color = ElementTheme.colors.textPrimary,
textAlign = TextAlign.Center,
)
SpaceInfoRow(
joinRule = joinRule,
numberOfRooms = numberOfRooms,
)
SpaceMembersView(
heroes = heroes,
numberOfMembers = numberOfMembers,
modifier = Modifier.padding(horizontal = 32.dp),
)
Text(
text = topic,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textPrimary,
textAlign = TextAlign.Center,
maxLines = topicMaxLines,
overflow = TextOverflow.Ellipsis,
)
}
}
@PreviewsDayNight
@Composable
internal fun SpaceHeaderViewPreview() = ElementPreview {
SpaceHeaderView(
avatarData = anAvatarData(
url = "anUrl",
size = AvatarSize.SpaceHeader,
),
name = "Space name",
topic = "Space topic: " + LoremIpsum(40).values.first(),
topicMaxLines = 2,
joinRule = JoinRule.Public,
heroes = persistentListOf(
aMatrixUser(id = "@1:d", displayName = "Alice", avatarUrl = "aUrl"),
aMatrixUser(id = "@2:d", displayName = "Bob"),
aMatrixUser(id = "@3:d", displayName = "Charlie", avatarUrl = "aUrl"),
aMatrixUser(id = "@4:d", displayName = "Dave"),
),
numberOfMembers = 999,
numberOfRooms = 10,
)
}

View file

@ -0,0 +1,135 @@
/*
* Copyright 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.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
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.TextAlign
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.theme.components.Icon
import io.element.android.libraries.matrix.api.room.join.JoinRule
import io.element.android.libraries.ui.strings.CommonPlurals
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun SpaceInfoRow(
leftText: String,
rightText: String,
modifier: Modifier = Modifier,
iconVector: ImageVector? = null,
) {
Row(
modifier = modifier,
horizontalArrangement = spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (iconVector != null) {
Icon(
modifier = Modifier.size(20.dp),
imageVector = iconVector,
contentDescription = null,
tint = ElementTheme.colors.iconTertiary,
)
}
val text = stringResource(id = CommonStrings.screen_space_list_details, leftText, rightText)
Text(
text = text,
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
}
}
@Composable
fun SpaceInfoRow(
joinRule: JoinRule,
numberOfRooms: Int,
modifier: Modifier = Modifier,
) {
val (leftText, rightText, icon) = when (joinRule) {
JoinRule.Public -> Triple(
stringResource(id = CommonStrings.common_public_space),
numberOfRooms(numberOfRooms),
CompoundIcons.Public(),
)
// TODO External space
// JoinRule.Private -> Triple(
// stringResource(id = CommonStrings.common_external_space),
// numberOfRooms(numberOfRooms),
// CompoundIcons.Guest(),
// )
// JoinRule.Private,
else -> Triple(
stringResource(id = CommonStrings.common_private_space),
numberOfRooms(numberOfRooms),
CompoundIcons.Lock(),
)
}
SpaceInfoRow(
leftText = leftText,
rightText = rightText,
modifier = modifier,
iconVector = icon,
)
}
@Composable
@ReadOnlyComposable
fun numberOfRooms(numberOfRooms: Int): String {
return pluralStringResource(CommonPlurals.common_rooms, numberOfRooms, numberOfRooms)
}
@Composable
@ReadOnlyComposable
fun numberOfSpaces(numberOfSpaces: Int): String {
return pluralStringResource(CommonPlurals.common_spaces, numberOfSpaces, numberOfSpaces)
}
@PreviewsDayNight
@Composable
internal fun SpaceInfoRowPreview() = ElementPreview {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = spacedBy(4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpaceInfoRow(
leftText = numberOfSpaces(5),
rightText = numberOfRooms(10),
)
SpaceInfoRow(
leftText = "Element space",
rightText = numberOfRooms(16),
iconVector = CompoundIcons.Workspace(),
)
SpaceInfoRow(
joinRule = JoinRule.Private,
numberOfRooms = 4,
)
SpaceInfoRow(
joinRule = JoinRule.Public,
numberOfRooms = 10,
)
}
}