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 a625c109b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 288 additions and 26 deletions

View file

@ -58,6 +58,7 @@ import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
@ -913,7 +914,7 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.success(
aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.KnockRestricted(emptyList())))
aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.KnockRestricted(persistentListOf())))
)
}
)
@ -933,7 +934,7 @@ class JoinRoomPresenterTest {
val client = FakeMatrixClient(
getNotJoinedRoomResult = { _, _ ->
Result.success(
aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Restricted(emptyList())))
aRoomPreview(info = aRoomPreviewInfo(joinRule = JoinRule.Restricted(persistentListOf())))
)
}
)

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,
)
}
}

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a4d295ae71ba2709845312b84983b79348069ded46b30091b9fa769a587cbb7
size 14337

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:da8f54591a52475c6f47fa083a1bb397e1164dd3e735562388fde4ff45644150
size 16275

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce1aba18ea8a6b45d9c40de9af961952e261c7c6200ab13cdfa67906d995208b
size 14888
oid sha256:8c6688901f89d3858ec67dda889fe589fb6b59202c64a4b700c2aa484e19f4cd
size 17606

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a4d295ae71ba2709845312b84983b79348069ded46b30091b9fa769a587cbb7
size 14337
oid sha256:26b9908bbd388321444037dcc1aae55037d3dabc7a9f9b14c39ba871f4f9d593
size 16128

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:da8f54591a52475c6f47fa083a1bb397e1164dd3e735562388fde4ff45644150
size 16275
oid sha256:1a96b6d95b8941d80035b309444b9eaa038098fb16aa84dec209fc3ee215ac9e
size 21687

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce1aba18ea8a6b45d9c40de9af961952e261c7c6200ab13cdfa67906d995208b
size 14888

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ebb657ce2227ccefcf9055c8c90342d6581ac0d916cd9f7e6a4990fa2bdc4177
size 61352

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:97c8ea5d2d83222ee16c0a27c783e04ec12dbd26aff427c28a21c9c581ad057d
size 60558

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0e4684cfbe7611d6c68879ddd7da5c57aa542b9f8b9250caf5f27009cabaadb2
size 22353

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4c7ad1861890262a67664d4e3b3e28d8a5d9ac4556ad029d1d5a387f61ac513f
size 21275